inbound-parser.test.ts
1 import buffers from './testing/test-buffers' 2 import BufferList from './testing/buffer-list' 3 import { parse } from '.' 4 import assert from 'assert' 5 import { PassThrough } from 'stream' 6 import { BackendMessage } from './messages' 7 8 const authOkBuffer = buffers.authenticationOk() 9 const paramStatusBuffer = buffers.parameterStatus('client_encoding', 'UTF8') 10 const readyForQueryBuffer = buffers.readyForQuery() 11 const backendKeyDataBuffer = buffers.backendKeyData(1, 2) 12 const commandCompleteBuffer = buffers.commandComplete('SELECT 3') 13 const parseCompleteBuffer = buffers.parseComplete() 14 const bindCompleteBuffer = buffers.bindComplete() 15 const portalSuspendedBuffer = buffers.portalSuspended() 16 17 const row1 = { 18 name: 'id', 19 tableID: 1, 20 attributeNumber: 2, 21 dataTypeID: 3, 22 dataTypeSize: 4, 23 typeModifier: 5, 24 formatCode: 0, 25 } 26 const oneRowDescBuff = buffers.rowDescription([row1]) 27 row1.name = 'bang' 28 29 const twoRowBuf = buffers.rowDescription([ 30 row1, 31 { 32 name: 'whoah', 33 tableID: 10, 34 attributeNumber: 11, 35 dataTypeID: 12, 36 dataTypeSize: 13, 37 typeModifier: 14, 38 formatCode: 0, 39 }, 40 ]) 41 42 const rowWithBigOids = { 43 name: 'bigoid', 44 tableID: 3000000001, 45 attributeNumber: 2, 46 dataTypeID: 3000000003, 47 dataTypeSize: 4, 48 typeModifier: 5, 49 formatCode: 0, 50 } 51 const bigOidDescBuff = buffers.rowDescription([rowWithBigOids]) 52 53 const emptyRowFieldBuf = buffers.dataRow([]) 54 55 const oneFieldBuf = buffers.dataRow(['test']) 56 57 const expectedAuthenticationOkayMessage = { 58 name: 'authenticationOk', 59 length: 8, 60 } 61 62 const expectedParameterStatusMessage = { 63 name: 'parameterStatus', 64 parameterName: 'client_encoding', 65 parameterValue: 'UTF8', 66 length: 25, 67 } 68 69 const expectedBackendKeyDataMessage = { 70 name: 'backendKeyData', 71 processID: 1, 72 secretKey: 2, 73 } 74 75 const expectedReadyForQueryMessage = { 76 name: 'readyForQuery', 77 length: 5, 78 status: 'I', 79 } 80 81 const expectedCommandCompleteMessage = { 82 name: 'commandComplete', 83 length: 13, 84 text: 'SELECT 3', 85 } 86 const emptyRowDescriptionBuffer = new BufferList() 87 .addInt16(0) // number of fields 88 .join(true, 'T') 89 90 const expectedEmptyRowDescriptionMessage = { 91 name: 'rowDescription', 92 length: 6, 93 fieldCount: 0, 94 fields: [], 95 } 96 const expectedOneRowMessage = { 97 name: 'rowDescription', 98 length: 27, 99 fieldCount: 1, 100 fields: [ 101 { 102 name: 'id', 103 tableID: 1, 104 columnID: 2, 105 dataTypeID: 3, 106 dataTypeSize: 4, 107 dataTypeModifier: 5, 108 format: 'text', 109 }, 110 ], 111 } 112 113 const expectedTwoRowMessage = { 114 name: 'rowDescription', 115 length: 53, 116 fieldCount: 2, 117 fields: [ 118 { 119 name: 'bang', 120 tableID: 1, 121 columnID: 2, 122 dataTypeID: 3, 123 dataTypeSize: 4, 124 dataTypeModifier: 5, 125 format: 'text', 126 }, 127 { 128 name: 'whoah', 129 tableID: 10, 130 columnID: 11, 131 dataTypeID: 12, 132 dataTypeSize: 13, 133 dataTypeModifier: 14, 134 format: 'text', 135 }, 136 ], 137 } 138 const expectedBigOidMessage = { 139 name: 'rowDescription', 140 length: 31, 141 fieldCount: 1, 142 fields: [ 143 { 144 name: 'bigoid', 145 tableID: 3000000001, 146 columnID: 2, 147 dataTypeID: 3000000003, 148 dataTypeSize: 4, 149 dataTypeModifier: 5, 150 format: 'text', 151 }, 152 ], 153 } 154 155 const emptyParameterDescriptionBuffer = new BufferList() 156 .addInt16(0) // number of parameters 157 .join(true, 't') 158 159 const oneParameterDescBuf = buffers.parameterDescription([1111]) 160 161 const twoParameterDescBuf = buffers.parameterDescription([2222, 3333]) 162 163 const expectedEmptyParameterDescriptionMessage = { 164 name: 'parameterDescription', 165 length: 6, 166 parameterCount: 0, 167 dataTypeIDs: [], 168 } 169 170 const expectedOneParameterMessage = { 171 name: 'parameterDescription', 172 length: 10, 173 parameterCount: 1, 174 dataTypeIDs: [1111], 175 } 176 177 const expectedTwoParameterMessage = { 178 name: 'parameterDescription', 179 length: 14, 180 parameterCount: 2, 181 dataTypeIDs: [2222, 3333], 182 } 183 184 const testForMessage = function (buffer: Buffer, expectedMessage: any) { 185 it('receives and parses ' + expectedMessage.name, async () => { 186 const messages = await parseBuffers([buffer]) 187 const [lastMessage] = messages 188 189 for (const key in expectedMessage) { 190 assert.deepEqual((lastMessage as any)[key], expectedMessage[key]) 191 } 192 }) 193 } 194 195 const plainPasswordBuffer = buffers.authenticationCleartextPassword() 196 const md5PasswordBuffer = buffers.authenticationMD5Password() 197 const SASLBuffer = buffers.authenticationSASL() 198 const SASLContinueBuffer = buffers.authenticationSASLContinue() 199 const SASLFinalBuffer = buffers.authenticationSASLFinal() 200 201 const expectedPlainPasswordMessage = { 202 name: 'authenticationCleartextPassword', 203 } 204 205 const expectedMD5PasswordMessage = { 206 name: 'authenticationMD5Password', 207 salt: Buffer.from([1, 2, 3, 4]), 208 } 209 210 const expectedSASLMessage = { 211 name: 'authenticationSASL', 212 mechanisms: ['SCRAM-SHA-256'], 213 } 214 215 const expectedSASLContinueMessage = { 216 name: 'authenticationSASLContinue', 217 data: 'data', 218 } 219 220 const expectedSASLFinalMessage = { 221 name: 'authenticationSASLFinal', 222 data: 'data', 223 } 224 225 const notificationResponseBuffer = buffers.notification(4, 'hi', 'boom') 226 const expectedNotificationResponseMessage = { 227 name: 'notification', 228 processId: 4, 229 channel: 'hi', 230 payload: 'boom', 231 } 232 233 const parseBuffers = async (buffers: Buffer[]): Promise<BackendMessage[]> => { 234 const stream = new PassThrough() 235 for (const buffer of buffers) { 236 stream.write(buffer) 237 } 238 stream.end() 239 const msgs: BackendMessage[] = [] 240 await parse(stream, (msg) => msgs.push(msg)) 241 return msgs 242 } 243 244 describe('PgPacketStream', function () { 245 testForMessage(authOkBuffer, expectedAuthenticationOkayMessage) 246 testForMessage(plainPasswordBuffer, expectedPlainPasswordMessage) 247 testForMessage(md5PasswordBuffer, expectedMD5PasswordMessage) 248 testForMessage(SASLBuffer, expectedSASLMessage) 249 testForMessage(SASLContinueBuffer, expectedSASLContinueMessage) 250 251 // this exercises a found bug in the parser: 252 // https://github.com/brianc/node-postgres/pull/2210#issuecomment-627626084 253 // and adds a test which is deterministic, rather than relying on network packet chunking 254 const extendedSASLContinueBuffer = Buffer.concat([SASLContinueBuffer, Buffer.from([1, 2, 3, 4])]) 255 testForMessage(extendedSASLContinueBuffer, expectedSASLContinueMessage) 256 257 testForMessage(SASLFinalBuffer, expectedSASLFinalMessage) 258 259 // this exercises a found bug in the parser: 260 // https://github.com/brianc/node-postgres/pull/2210#issuecomment-627626084 261 // and adds a test which is deterministic, rather than relying on network packet chunking 262 const extendedSASLFinalBuffer = Buffer.concat([SASLFinalBuffer, Buffer.from([1, 2, 4, 5])]) 263 testForMessage(extendedSASLFinalBuffer, expectedSASLFinalMessage) 264 265 testForMessage(paramStatusBuffer, expectedParameterStatusMessage) 266 testForMessage(backendKeyDataBuffer, expectedBackendKeyDataMessage) 267 testForMessage(readyForQueryBuffer, expectedReadyForQueryMessage) 268 testForMessage(commandCompleteBuffer, expectedCommandCompleteMessage) 269 testForMessage(notificationResponseBuffer, expectedNotificationResponseMessage) 270 testForMessage(buffers.emptyQuery(), { 271 name: 'emptyQuery', 272 length: 4, 273 }) 274 275 testForMessage(Buffer.from([0x6e, 0, 0, 0, 4]), { 276 name: 'noData', 277 }) 278 279 describe('rowDescription messages', function () { 280 testForMessage(emptyRowDescriptionBuffer, expectedEmptyRowDescriptionMessage) 281 testForMessage(oneRowDescBuff, expectedOneRowMessage) 282 testForMessage(twoRowBuf, expectedTwoRowMessage) 283 testForMessage(bigOidDescBuff, expectedBigOidMessage) 284 }) 285 286 describe('parameterDescription messages', function () { 287 testForMessage(emptyParameterDescriptionBuffer, expectedEmptyParameterDescriptionMessage) 288 testForMessage(oneParameterDescBuf, expectedOneParameterMessage) 289 testForMessage(twoParameterDescBuf, expectedTwoParameterMessage) 290 }) 291 292 describe('parsing rows', function () { 293 describe('parsing empty row', function () { 294 testForMessage(emptyRowFieldBuf, { 295 name: 'dataRow', 296 fieldCount: 0, 297 }) 298 }) 299 300 describe('parsing data row with fields', function () { 301 testForMessage(oneFieldBuf, { 302 name: 'dataRow', 303 fieldCount: 1, 304 fields: ['test'], 305 }) 306 }) 307 }) 308 309 describe('notice message', function () { 310 // this uses the same logic as error message 311 const buff = buffers.notice([{ type: 'C', value: 'code' }]) 312 testForMessage(buff, { 313 name: 'notice', 314 code: 'code', 315 }) 316 }) 317 318 testForMessage(buffers.error([]), { 319 name: 'error', 320 }) 321 322 describe('with all the fields', function () { 323 const buffer = buffers.error([ 324 { 325 type: 'S', 326 value: 'ERROR', 327 }, 328 { 329 type: 'C', 330 value: 'code', 331 }, 332 { 333 type: 'M', 334 value: 'message', 335 }, 336 { 337 type: 'D', 338 value: 'details', 339 }, 340 { 341 type: 'H', 342 value: 'hint', 343 }, 344 { 345 type: 'P', 346 value: '100', 347 }, 348 { 349 type: 'p', 350 value: '101', 351 }, 352 { 353 type: 'q', 354 value: 'query', 355 }, 356 { 357 type: 'W', 358 value: 'where', 359 }, 360 { 361 type: 'F', 362 value: 'file', 363 }, 364 { 365 type: 'L', 366 value: 'line', 367 }, 368 { 369 type: 'R', 370 value: 'routine', 371 }, 372 { 373 type: 'Z', // ignored 374 value: 'alsdkf', 375 }, 376 ]) 377 378 testForMessage(buffer, { 379 name: 'error', 380 severity: 'ERROR', 381 code: 'code', 382 message: 'message', 383 detail: 'details', 384 hint: 'hint', 385 position: '100', 386 internalPosition: '101', 387 internalQuery: 'query', 388 where: 'where', 389 file: 'file', 390 line: 'line', 391 routine: 'routine', 392 }) 393 }) 394 395 testForMessage(parseCompleteBuffer, { 396 name: 'parseComplete', 397 }) 398 399 testForMessage(bindCompleteBuffer, { 400 name: 'bindComplete', 401 }) 402 403 testForMessage(bindCompleteBuffer, { 404 name: 'bindComplete', 405 }) 406 407 testForMessage(buffers.closeComplete(), { 408 name: 'closeComplete', 409 }) 410 411 describe('parses portal suspended message', function () { 412 testForMessage(portalSuspendedBuffer, { 413 name: 'portalSuspended', 414 }) 415 }) 416 417 describe('parses replication start message', function () { 418 testForMessage(Buffer.from([0x57, 0x00, 0x00, 0x00, 0x04]), { 419 name: 'replicationStart', 420 length: 4, 421 }) 422 }) 423 424 describe('copy', () => { 425 testForMessage(buffers.copyIn(0), { 426 name: 'copyInResponse', 427 length: 7, 428 binary: false, 429 columnTypes: [], 430 }) 431 432 testForMessage(buffers.copyIn(2), { 433 name: 'copyInResponse', 434 length: 11, 435 binary: false, 436 columnTypes: [0, 1], 437 }) 438 439 testForMessage(buffers.copyOut(0), { 440 name: 'copyOutResponse', 441 length: 7, 442 binary: false, 443 columnTypes: [], 444 }) 445 446 testForMessage(buffers.copyOut(3), { 447 name: 'copyOutResponse', 448 length: 13, 449 binary: false, 450 columnTypes: [0, 1, 2], 451 }) 452 453 testForMessage(buffers.copyDone(), { 454 name: 'copyDone', 455 length: 4, 456 }) 457 458 testForMessage(buffers.copyData(Buffer.from([5, 6, 7])), { 459 name: 'copyData', 460 length: 7, 461 chunk: Buffer.from([5, 6, 7]), 462 }) 463 }) 464 465 // since the data message on a stream can randomly divide the incomming 466 // tcp packets anywhere, we need to make sure we can parse every single 467 // split on a tcp message 468 describe('split buffer, single message parsing', function () { 469 const fullBuffer = buffers.dataRow([null, 'bang', 'zug zug', null, '!']) 470 471 it('parses when full buffer comes in', async function () { 472 const messages = await parseBuffers([fullBuffer]) 473 const message = messages[0] as any 474 assert.equal(message.fields.length, 5) 475 assert.equal(message.fields[0], null) 476 assert.equal(message.fields[1], 'bang') 477 assert.equal(message.fields[2], 'zug zug') 478 assert.equal(message.fields[3], null) 479 assert.equal(message.fields[4], '!') 480 }) 481 482 const testMessageReceivedAfterSplitAt = async function (split: number) { 483 const firstBuffer = Buffer.alloc(fullBuffer.length - split) 484 const secondBuffer = Buffer.alloc(fullBuffer.length - firstBuffer.length) 485 fullBuffer.copy(firstBuffer, 0, 0) 486 fullBuffer.copy(secondBuffer, 0, firstBuffer.length) 487 const messages = await parseBuffers([firstBuffer, secondBuffer]) 488 const message = messages[0] as any 489 assert.equal(message.fields.length, 5) 490 assert.equal(message.fields[0], null) 491 assert.equal(message.fields[1], 'bang') 492 assert.equal(message.fields[2], 'zug zug') 493 assert.equal(message.fields[3], null) 494 assert.equal(message.fields[4], '!') 495 } 496 497 it('parses when split in the middle', function () { 498 return testMessageReceivedAfterSplitAt(6) 499 }) 500 501 it('parses when split at end', function () { 502 return testMessageReceivedAfterSplitAt(2) 503 }) 504 505 it('parses when split at beginning', function () { 506 return Promise.all([ 507 testMessageReceivedAfterSplitAt(fullBuffer.length - 2), 508 testMessageReceivedAfterSplitAt(fullBuffer.length - 1), 509 testMessageReceivedAfterSplitAt(fullBuffer.length - 5), 510 ]) 511 }) 512 }) 513 514 describe('split buffer, multiple message parsing', function () { 515 const dataRowBuffer = buffers.dataRow(['!']) 516 const readyForQueryBuffer = buffers.readyForQuery() 517 const fullBuffer = Buffer.alloc(dataRowBuffer.length + readyForQueryBuffer.length) 518 dataRowBuffer.copy(fullBuffer, 0, 0) 519 readyForQueryBuffer.copy(fullBuffer, dataRowBuffer.length, 0) 520 521 const verifyMessages = function (messages: any[]) { 522 assert.strictEqual(messages.length, 2) 523 assert.deepEqual(messages[0], { 524 name: 'dataRow', 525 fieldCount: 1, 526 length: 11, 527 fields: ['!'], 528 }) 529 assert.equal(messages[0].fields[0], '!') 530 assert.deepEqual(messages[1], { 531 name: 'readyForQuery', 532 length: 5, 533 status: 'I', 534 }) 535 } 536 // sanity check 537 it('receives both messages when packet is not split', async function () { 538 const messages = await parseBuffers([fullBuffer]) 539 verifyMessages(messages) 540 }) 541 542 const splitAndVerifyTwoMessages = async function (split: number) { 543 const firstBuffer = Buffer.alloc(fullBuffer.length - split) 544 const secondBuffer = Buffer.alloc(fullBuffer.length - firstBuffer.length) 545 fullBuffer.copy(firstBuffer, 0, 0) 546 fullBuffer.copy(secondBuffer, 0, firstBuffer.length) 547 const messages = await parseBuffers([firstBuffer, secondBuffer]) 548 verifyMessages(messages) 549 } 550 551 describe('receives both messages when packet is split', function () { 552 it('in the middle', function () { 553 return splitAndVerifyTwoMessages(11) 554 }) 555 it('at the front', function () { 556 return Promise.all([ 557 splitAndVerifyTwoMessages(fullBuffer.length - 1), 558 splitAndVerifyTwoMessages(fullBuffer.length - 4), 559 splitAndVerifyTwoMessages(fullBuffer.length - 6), 560 ]) 561 }) 562 563 it('at the end', function () { 564 return Promise.all([splitAndVerifyTwoMessages(8), splitAndVerifyTwoMessages(1)]) 565 }) 566 }) 567 }) 568 })