diff --git a/packages/gaussdb-node/esm/index.mjs b/packages/gaussdb-node/esm/index.mjs index 30eeba3fa..2e5b3f4bb 100644 --- a/packages/gaussdb-node/esm/index.mjs +++ b/packages/gaussdb-node/esm/index.mjs @@ -12,6 +12,8 @@ export const escapeIdentifier = gaussdb.escapeIdentifier export const escapeLiteral = gaussdb.escapeLiteral export const Result = gaussdb.Result export const TypeOverrides = gaussdb.TypeOverrides +export const LogicalReplicationService = gaussdb.LogicalReplicationService +export const MppdbDecodingPlugin = gaussdb.MppdbDecodingPlugin // Also export the defaults export const defaults = gaussdb.defaults diff --git a/packages/gaussdb-node/lib/index.js b/packages/gaussdb-node/lib/index.js index 8c99047fe..31923aa39 100644 --- a/packages/gaussdb-node/lib/index.js +++ b/packages/gaussdb-node/lib/index.js @@ -8,6 +8,8 @@ const utils = require('./utils') const Pool = require('gaussdb-pool') const TypeOverrides = require('./type-overrides') const { DatabaseError } = require('gaussdb-protocol') +const { LogicalReplicationService, MppdbDecodingPlugin } = require('./logical-replication') + const { escapeIdentifier, escapeLiteral } = require('./utils') const poolFactory = (Client) => { @@ -28,6 +30,8 @@ const GAUSSDB = function (clientConstructor) { this.types = require('pg-types') this.DatabaseError = DatabaseError this.TypeOverrides = TypeOverrides + this.LogicalReplicationService = LogicalReplicationService + this.MppdbDecodingPlugin = MppdbDecodingPlugin this.escapeIdentifier = escapeIdentifier this.escapeLiteral = escapeLiteral this.Result = Result diff --git a/packages/gaussdb-node/lib/logical-replication/index.js b/packages/gaussdb-node/lib/logical-replication/index.js new file mode 100644 index 000000000..a219fbefc --- /dev/null +++ b/packages/gaussdb-node/lib/logical-replication/index.js @@ -0,0 +1,9 @@ +'use strict' + +const LogicalReplicationService = require('./logical-replication-service') +const MppdbDecodingPlugin = require('./mppdb-decoding-plugin') + +module.exports = { + LogicalReplicationService, + MppdbDecodingPlugin, +} diff --git a/packages/gaussdb-node/lib/logical-replication/logical-replication-service.js b/packages/gaussdb-node/lib/logical-replication/logical-replication-service.js new file mode 100644 index 000000000..a3d4bac31 --- /dev/null +++ b/packages/gaussdb-node/lib/logical-replication/logical-replication-service.js @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2025 happy-game + * + * This source code is derived from and/or based on: + * pg-logical-replication - Copyright (c) 2025 Kibae Shin + * + * Licensed under the MIT License. + */ +'use strict' +const EventEmitter = require('events').EventEmitter +const Client = require('../client') +const { BufferReader } = require('gaussdb-protocol') +const POSTGRES_EPOCH_MS = 946684800000 +const MAX_UINT64 = { hi: 0x7fffffff, lo: 0xffffffff } + +class LogicalReplicationService extends EventEmitter { + constructor(clientConfig, config) { + super() + this._lastLsn = null + this._lastReceive = null + this._lastFlushed = null + this._lastApplied = null + this._client = null + this._connection = null + this._stop = true + // Flow control (backpressure) queue + this._messageQueue = [] + this._processing = false + this._lastStandbyStatusUpdatedTime = 0 + this._checkStandbyStatusTimer = null + this.clientConfig = clientConfig + this.config = { + acknowledge: Object.assign( + { + // If the value is false, acknowledge must be done manually. Default: true + auto: true, + // Acknowledge is performed every set time (sec). If 0, do not do it. Default: 10 + timeoutSeconds: 10, + }, + (config && config.acknowledge) || {} + ), + // Flow control (backpressure) configuration. + // When enabled, the stream will be paused until the data handler completes, + // preventing memory overflow when processing is slower than the incoming message rate. + flowControl: Object.assign( + { + // If true, pause the stream until the data handler completes. Default: false + enabled: false, + }, + (config && config.flowControl) || {} + ), + } + } + + lastLsn() { + return this._lastLsn || '0/00000000' + } + + async stop() { + this._stop = true + // Clear flow control queue + this._messageQueue = [] + this._processing = false + if (this._connection) { + this._connection.removeAllListeners() + this._connection = null + } + if (this._client) { + this._client.removeAllListeners() + await this._client.end() + this._client = null + } + this._checkStandbyStatus(false) + return this + } + + /** + * @param plugin One of [MppdbDecodingPlugin, ] + * @param slotName + * @param uptoLsn + */ + async subscribe(plugin, slotName, uptoLsn) { + try { + const [client, connection] = await this._createClient() + this._lastLsn = uptoLsn || this._lastLsn + + // check replicationStart + connection.once('replicationStart', () => { + this._stop = false + this.emit('start') + this._checkStandbyStatus(true) + }) + + connection.on('copyData', (msg) => { + const buffer = msg && msg.chunk ? msg.chunk : msg + if (!buffer || buffer.length === 0) return + this._handleCopyData(plugin, buffer) + }) + return plugin.start(client, slotName, this._lastLsn || '0/00000000') + } catch (e) { + await this.stop() + this.emit('error', e) + throw e + } + } + + /** + * OpenGauss uses a 65-byte little-endian Standby Status Update packet, + * different from PostgreSQL's 34-byte big-endian format. + * @param lsn + * @param ping Request server to respond + */ + async acknowledge(lsn, ping) { + if (this._stop || !this._connection) return false + const received = lsn ? parseLsn(lsn) : this._lastReceive || { hi: 0, lo: 0 } + const flushed = this._lastFlushed || received + const applied = this._lastApplied || received + this._lastStandbyStatusUpdatedTime = Date.now() + + // Timestamp as microseconds since midnight 2000-01-01 + const nowMicros = (Date.now() - POSTGRES_EPOCH_MS) * 1000 + const timeHi = Math.floor(nowMicros / 0x100000000) + const timeLo = Math.floor(nowMicros - timeHi * 0x100000000) + + const response = Buffer.alloc(65) + let offset = 0 + response[offset++] = 0x72 // 'r' + offset = writeUInt64LE(response, offset, MAX_UINT64) // sendTime (unused, set to max) + offset = writeUInt64LE(response, offset, received) // Last WAL received + offset = writeUInt64LE(response, offset, flushed) // Last WAL flushed to disk + offset = writeUInt64LE(response, offset, MAX_UINT64) // flushTime (unused, set to max) + offset = writeUInt64LE(response, offset, applied) // Last WAL applied + response.writeUInt32LE(0xffffffff, offset) // applyTime lo (unused) + offset += 4 + response.writeUInt32LE(0xffffffff, offset) // applyTime hi (unused) + offset += 4 + offset = writeUInt64LE(response, offset, { hi: timeHi >>> 0, lo: timeLo >>> 0 }) // client timestamp + // If 1, requests server to respond immediately - can be used to verify connectivity + response[offset++] = ping ? 1 : received.hi === 0 && received.lo === 0 ? 1 : 0 + response.writeUInt32LE(0, offset) // xlogFlushLocation (unused) + offset += 4 + response[offset++] = 1 // peer_role + response[offset++] = 1 // peer_state + response[offset++] = 1 // sender_sent_location flag + this._connection.sendCopyFromChunk(response) + return true + } + + setFlushedLsn(lsn) { + this._lastFlushed = parseLsn(lsn) + } + + setAppliedLsn(lsn) { + this._lastApplied = parseLsn(lsn) + } + + async _createClient() { + await this.stop() + this._client = new Client(Object.assign({}, this.clientConfig, { replication: 'database' })) + await this._client.connect() + this._connection = this._client.connection + this._client.on('error', (e) => this.emit('error', e)) + return [this._client, this._connection] + } + + _handleCopyData(plugin, buffer) { + const tag = buffer[0] + if (tag !== 0x77 && tag !== 0x6b) { + return + } + if (tag === 0x77) { + // XLogData: OpenGauss uses big-endian LSN in header (same as PostgreSQL) + const reader = new BufferReader(1, 'be') + reader.setBuffer(1, buffer) + const start = reader.uint64Parts() + const lsn = formatLsn(start.hi, start.lo) + this._updateLastReceive(start) + const parsed = plugin.parse(buffer.slice(25)) + if (Array.isArray(parsed)) { + for (const item of parsed) { + const itemLsn = item && item.lsn ? item.lsn : lsn + this._enqueue(itemLsn, item) + } + } else { + this._enqueue(lsn, parsed) + } + } + if (tag === 0x6b) { + // Primary keepalive message: OpenGauss uses little-endian and includes extra server mode/state fields + const reader = new BufferReader(1, 'le') + reader.setBuffer(1, buffer) + const server = reader.uint64Parts() + if (!this._lastReceive || compareLsn(server, this._lastReceive) > 0) { + this._updateLastReceive(server) + } + reader.setBuffer(17, buffer) + const serverClock = reader.uint64Parts() + const timestamp = serverClockToTimestamp(serverClock) + const shouldRespond = buffer[25] === 1 + this.emit('heartbeat', formatLsn(server.hi, server.lo), timestamp, shouldRespond) + if (shouldRespond) { + this.acknowledge(this._lastLsn, true) + } + } + } + + async _acknowledge(lsn) { + if (!this.config.acknowledge.auto) return + this.emit('acknowledge', lsn) + await this.acknowledge(lsn) + } + + _enqueue(lsn, data) { + this._lastLsn = lsn + this._updateLastReceive(parseLsn(lsn)) + if (!this.config.flowControl.enabled) { + this.emit('data', lsn, data) + this._acknowledge(lsn) + return + } + this._messageQueue.push({ lsn, data }) + this._processQueue() + } + + /** + * Process messages in the queue sequentially with backpressure support. + * Pauses the stream while processing and resumes when the queue is empty. + */ + _processQueue() { + if (this._processing || this._stop) return + this._processing = true + + // Pause the stream to prevent buffer overflow + if (this._connection && this._connection.stream && this._connection.stream.pause) { + this._connection.stream.pause() + } + + const processNext = async () => { + while (this._messageQueue.length > 0 && !this._stop) { + const message = this._messageQueue.shift() + try { + // Wait for all listeners to complete (supports async handlers) + await this._emitAsync('data', message.lsn, message.data) + await this._acknowledge(message.lsn) + } catch (e) { + this.emit('error', e) + } + } + this._processing = false + + // Resume the stream when queue is empty + if (!this._stop && this._connection && this._connection.stream && this._connection.stream.resume) { + this._connection.stream.resume() + } + } + processNext() + } + + async _emitAsync(event, ...args) { + const listeners = this.listeners(event) + for (const listener of listeners) { + await listener(...args) + } + } + + _checkStandbyStatus(enable) { + if (this._checkStandbyStatusTimer) { + clearInterval(this._checkStandbyStatusTimer) + this._checkStandbyStatusTimer = null + } + if (this.config.acknowledge.timeoutSeconds > 0 && enable) { + this._checkStandbyStatusTimer = setInterval(async () => { + if (this._stop) return + if ( + this._lastLsn && + Date.now() - this._lastStandbyStatusUpdatedTime > this.config.acknowledge.timeoutSeconds * 1000 + ) { + await this.acknowledge(this._lastLsn) + } + }, 1000) + } + } + + _updateLastReceive(parts) { + this._lastReceive = { hi: parts.hi >>> 0, lo: parts.lo >>> 0 } + this._lastLsn = formatLsn(this._lastReceive.hi, this._lastReceive.lo) + } +} + +function writeUInt64LE(buffer, offset, parts) { + buffer.writeUInt32LE(parts.lo >>> 0, offset) + buffer.writeUInt32LE(parts.hi >>> 0, offset + 4) + return offset + 8 +} + +function parseLsn(lsn) { + if (!lsn) return { hi: 0, lo: 0 } + const parts = String(lsn).split('/') + if (parts.length !== 2) return { hi: 0, lo: 0 } + const hi = parseInt(parts[0], 16) + const lo = parseInt(parts[1], 16) + return { hi: hi >>> 0, lo: lo >>> 0 } +} + +function pad8(value) { + const hex = (value >>> 0).toString(16).toUpperCase() + return ('00000000' + hex).slice(-8) +} + +function formatLsn(hi, lo) { + return `${pad8(hi)}/${pad8(lo)}` +} + +function compareLsn(a, b) { + if (a.hi !== b.hi) return a.hi > b.hi ? 1 : -1 + if (a.lo === b.lo) return 0 + return a.lo > b.lo ? 1 : -1 +} + +function serverClockToTimestamp(parts) { + const micros = uint64ToNumberOrString(parts) + if (typeof micros !== 'number') return null + return Math.floor(micros / 1000) + POSTGRES_EPOCH_MS +} + +function uint64ToNumberOrString(parts) { + const hi = parts.hi >>> 0 + const lo = parts.lo >>> 0 + if (hi <= 0x1fffff) { + return hi * 0x100000000 + lo + } + return `0x${pad8(hi)}${pad8(lo)}` +} + +module.exports = LogicalReplicationService diff --git a/packages/gaussdb-node/lib/logical-replication/mppdb-decoding-plugin.js b/packages/gaussdb-node/lib/logical-replication/mppdb-decoding-plugin.js new file mode 100644 index 000000000..a32069bfd --- /dev/null +++ b/packages/gaussdb-node/lib/logical-replication/mppdb-decoding-plugin.js @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2025 happy-game + * + * This source code is derived from and/or based on: + * pg-logical-replication - Copyright (c) 2025 Kibae Shin + * + * Licensed under the MIT License. + */ +'use strict' + +const { BufferReader } = require('gaussdb-protocol') + +const SLOT_OPTION_KEYS = [ + 'includeXids', + 'skipEmptyXacts', + 'includeTimestamp', + 'onlyLocal', + 'forceBinary', + 'whiteTableList', + 'parallelDecodeNum', + 'decodeStyle', + 'sendingBatch', + 'standbyConnection', + 'parallelQueueSize', + 'maxTxnInMemory', + 'maxReorderbufferInMemory', + 'senderTimeout', +] + +const STRING_OPTION_KEYS = ['whiteTableList', 'decodeStyle'] + +/** + * Plugin for parsing mppdb_decoding logical replication output from OpenGauss/GaussDB. + * Supports text ('t'), JSON ('j'), and binary ('b') decode styles. + */ +class MppdbDecodingPlugin { + constructor(options) { + this.options = options || {} + } + + get name() { + return 'mppdb_decoding' + } + + /** + * @param client GaussDB client connection + * @param slotName Replication slot name + * @param lastLsn LSN to start replication from + */ + async start(client, slotName, lastLsn) { + const options = this._buildSlotOptions() + let sql = `START_REPLICATION SLOT "${slotName}" LOGICAL ${lastLsn}` + if (options.length > 0) { + sql += ` (${options.join(' , ')})` + } + return client.query({ text: sql, queryMode: 'simple' }) + } + + parse(buffer) { + const decodeStyle = this._decodeStyle() + if (decodeStyle === 'b') { + return this._parseBinary(buffer) + } + return this._parseText(buffer, decodeStyle) + } + + _decodeStyle() { + const slotOptions = this.options.slotOptions || {} + return this.options.decodeStyle || this.options['decode-style'] || slotOptions['decode-style'] || 't' + } + + _sendingBatch() { + const slotOptions = this.options.slotOptions || {} + const value = this.options.sendingBatch || this.options['sending-batch'] || slotOptions['sending-batch'] + if (value === undefined) return false + if (typeof value === 'string') return value === '1' || value.toLowerCase() === 'true' + return Boolean(value) + } + + _payloadEndian() { + const value = this.options.payloadEndian || this.options['payload-endian'] + return value === 'le' ? 'le' : 'be' + } + + _buildSlotOptions() { + const slotOptions = Object.assign({}, this.options.slotOptions || {}) + for (const key of SLOT_OPTION_KEYS) { + if (this.options[key] !== undefined) { + slotOptions[dashCase(key)] = this.options[key] + } + } + + const options = [] + for (const [key, value] of Object.entries(slotOptions)) { + const strValue = formatSlotValue(key, value) + options.push(`"${key}" '${escapeLiteral(strValue)}'`) + } + return options + } + + // Batch format: uint32 len, uint64 lsn, payload bytes (len), repeat until len=0. + _parseText(buffer, decodeStyle) { + if (!this._sendingBatch()) { + const text = buffer.toString('utf8') + if (decodeStyle === 'j') { + return tryParseJson(text) + } + return text + } + + const reader = new BufferReader(0, this._payloadEndian()) + reader.setBuffer(0, buffer) + const items = [] + while (reader.remaining() >= 4) { + const len = reader.uint32() + if (len === 0) break + const lsnParts = reader.uint64Parts() + const payload = reader.bytes(len).toString('utf8') + const data = decodeStyle === 'j' ? tryParseJson(payload) : payload + items.push({ lsn: formatLsn(lsnParts.hi, lsnParts.lo), payload: data }) + } + return items + } + + // Binary record tags: B=BEGIN, C=COMMIT, I=INSERT, U=UPDATE, D=DELETE + _parseBinary(buffer) { + const reader = new BufferReader(0, this._payloadEndian()) + reader.setBuffer(0, buffer) + const items = [] + + while (reader.remaining() >= 4) { + const len = reader.uint32() + if (len === 0) break + + const lsnParts = reader.uint64Parts() + const recordBuf = reader.bytes(len) + // Length excludes the statement separator (P/F), which follows the record. + const separator = reader.remaining() > 0 ? reader.byte() : null + + const record = parseBinaryRecord(recordBuf, this._payloadEndian()) + items.push({ + lsn: formatLsn(lsnParts.hi, lsnParts.lo), + batchEnd: separator === 0x46, + record, + }) + } + + return items + } +} + +function parseBinaryRecord(buffer, endian) { + const reader = new BufferReader(0, endian) + reader.setBuffer(0, buffer) + const tagByte = reader.byte() + const tag = String.fromCharCode(tagByte) + + switch (tag) { + case 'B': + return parseBegin(reader) + case 'C': + return parseCommit(reader) + case 'I': + return parseDml(reader, 'insert') + case 'U': + return parseDml(reader, 'update') + case 'D': + return parseDml(reader, 'delete') + default: + throw new Error(`unknown mppdb_decoding tag: ${tag}`) + } +} + +function parseBegin(reader) { + const csn = uint64ToNumberOrString(reader.uint64Parts()) + const firstLsnParts = reader.uint64Parts() + const info = { + tag: 'begin', + csn, + firstLsn: formatLsn(firstLsnParts.hi, firstLsnParts.lo), + } + return Object.assign(info, parseOptionalTxnFields(reader)) +} + +function parseCommit(reader) { + const info = { tag: 'commit' } + return Object.assign(info, parseOptionalTxnFields(reader)) +} + +// Optional fields: 0x58='X' for xid, 0x54='T' for timestamp +function parseOptionalTxnFields(reader) { + const info = {} + while (reader.remaining() > 0) { + const next = reader.peekByte() + if (next === 0x58) { + // 'X' for xid + reader.byte() + info.xid = uint64ToNumberOrString(reader.uint64Parts()) + continue + } + if (next === 0x54) { + // 'T' for timestamp + reader.byte() + const tsLen = reader.uint32() + info.timestamp = reader.string(tsLen) + continue + } + break + } + return info +} + +// Tuple types: 0x4e='N' for new, 0x4f='O' for old +function parseDml(reader, tag) { + const schemaLen = reader.uint16() + const schema = reader.string(schemaLen) + const tableLen = reader.uint16() + const table = reader.string(tableLen) + + const tuples = [] + while (reader.remaining() > 0) { + const tupleType = reader.byte() + if (tupleType !== 0x4e && tupleType !== 0x4f) { + throw new Error(`unknown tuple tag: ${String.fromCharCode(tupleType)}`) + } + const tuple = { + kind: tupleType === 0x4e ? 'new' : 'old', + columns: [], + } + + const attrCount = reader.uint16() + for (let i = 0; i < attrCount; i += 1) { + const nameLen = reader.uint16() + const name = reader.string(nameLen) + const typeOid = reader.uint32() + const valueLen = reader.uint32() + let value = null + // 0xFFFFFFFF indicates NULL, 0 indicates empty string. + if (valueLen !== 0xffffffff) { + value = reader.string(valueLen) + } + tuple.columns.push({ name, typeOid, value }) + } + + tuples.push(tuple) + } + + return { tag, schema, table, tuples } +} + +function tryParseJson(text) { + try { + return JSON.parse(text) + } catch (e) { + return text + } +} + +function formatSlotValue(key, value) { + if (typeof value === 'boolean') { + return value ? '1' : '0' + } + if (STRING_OPTION_KEYS.includes(key)) { + return value == null ? '' : String(value) + } + return value == null ? '' : String(value) +} + +function escapeLiteral(value) { + return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'") +} + +function dashCase(str) { + return (str || '').replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) +} + +function pad8(value) { + const hex = (value >>> 0).toString(16).toUpperCase() + return ('00000000' + hex).slice(-8) +} + +function formatLsn(hi, lo) { + return `${pad8(hi)}/${pad8(lo)}` +} + +function uint64ToNumberOrString(parts) { + const hi = parts.hi >>> 0 + const lo = parts.lo >>> 0 + if (hi <= 0x1fffff) { + return hi * 0x100000000 + lo + } + return `0x${pad8(hi)}${pad8(lo)}` +} + +module.exports = MppdbDecodingPlugin diff --git a/packages/gaussdb-node/test/integration/client/sha256-password-tests.js b/packages/gaussdb-node/test/integration/client/sha256-password-tests.js index fd356fd00..bc936023c 100644 --- a/packages/gaussdb-node/test/integration/client/sha256-password-tests.js +++ b/packages/gaussdb-node/test/integration/client/sha256-password-tests.js @@ -48,6 +48,9 @@ suite.testAsync('can connect using sha256 password authentication', async () => }) await client.connect() + // Verify SHA256 authentication was used + assert.strictEqual(usingSha256, true, 'Should use SHA256 authentication') + // Test basic query execution const { rows } = await client.query('SELECT NOW()') assert.strictEqual(rows.length, 1) @@ -60,10 +63,6 @@ suite.testAsync('sha256 authentication fails when password is wrong', async () = ...config, password: config.password + 'append-something-to-make-it-bad', }) - let usingSha256 = false - client.connection.once('authenticationSHA256Password', () => { - usingSha256 = true - }) await assert.rejects( () => client.connect(), { @@ -79,10 +78,6 @@ suite.testAsync('sha256 authentication fails when password is empty', async () = // use a password function to simulate empty password password: () => '', }) - let usingSha256 = false - client.connection.once('authenticationSHA256Password', () => { - usingSha256 = true - }) await assert.rejects( () => client.connect(), (err) => { @@ -91,4 +86,4 @@ suite.testAsync('sha256 authentication fails when password is empty', async () = }, 'Should fail with password-related error' ) -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-node/test/integration/gh-issues/1105-tests.js b/packages/gaussdb-node/test/integration/gh-issues/1105-tests.js index a45dffc85..c905408ad 100644 --- a/packages/gaussdb-node/test/integration/gh-issues/1105-tests.js +++ b/packages/gaussdb-node/test/integration/gh-issues/1105-tests.js @@ -18,4 +18,4 @@ suite.testAsync('timeout causing query crashes', async () => { } } await client.end() -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-node/test/unit/crypto/crypto-cert-tests.js b/packages/gaussdb-node/test/unit/crypto/crypto-cert-tests.js index 88ad2c207..7d3f13eda 100644 --- a/packages/gaussdb-node/test/unit/crypto/crypto-cert-tests.js +++ b/packages/gaussdb-node/test/unit/crypto/crypto-cert-tests.js @@ -6,21 +6,29 @@ const suite = new helper.Suite() // 测试证书签名功能 suite.testAsync('signatureAlgorithmHashFromCertificate function', async () => { const { signatureAlgorithmHashFromCertificate } = require('../../../lib/crypto/cert-signatures') - + // 测试函数存在性 assert.strictEqual(typeof signatureAlgorithmHashFromCertificate, 'function') - + // 创建一个简单的模拟证书数据(简化版X.509结构) // 这是一个非常简化的测试,实际证书结构要复杂得多 const certData = Buffer.from([ 0x30, // SEQUENCE - 0x0C, // 长度 + 0x0c, // 长度 0x06, // OID 0x08, // OID长度 - 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x02, 0x05, // SHA256 OID - 0x05, 0x00 // NULL + 0x2a, + 0x86, + 0x48, + 0x86, + 0xf7, + 0x0d, + 0x02, + 0x05, // SHA256 OID + 0x05, + 0x00, // NULL ]) - + // 测试正常情况 try { const result = signatureAlgorithmHashFromCertificate(certData, 0) @@ -34,13 +42,13 @@ suite.testAsync('signatureAlgorithmHashFromCertificate function', async () => { suite.testAsync('x509Error function', async () => { const certSignatures = require('../../../lib/crypto/cert-signatures') - + // 由于x509Error是内部函数,我们无法直接测试它 // 但我们可以通过测试其他会调用它的函数来间接测试 - + // 创建无效的证书数据来触发错误 - const invalidCertData = Buffer.from([0xFF, 0xFF]) // 无效的数据 - + const invalidCertData = Buffer.from([0xff, 0xff]) // 无效的数据 + try { certSignatures.signatureAlgorithmHashFromCertificate(invalidCertData, 0) // 如果没有抛出错误,测试失败 @@ -52,11 +60,10 @@ suite.testAsync('x509Error function', async () => { } }) - // 测试边界情况和错误处理 suite.testAsync('certificate functions edge cases', async () => { const { signatureAlgorithmHashFromCertificate } = require('../../../lib/crypto/cert-signatures') - + // 测试空缓冲区 try { signatureAlgorithmHashFromCertificate(Buffer.alloc(0), 0) @@ -64,7 +71,7 @@ suite.testAsync('certificate functions edge cases', async () => { } catch (err) { assert(err instanceof Error) } - + // 测试null输入 try { signatureAlgorithmHashFromCertificate(null, 0) @@ -72,7 +79,7 @@ suite.testAsync('certificate functions edge cases', async () => { } catch (err) { assert(err instanceof Error) } - + // 测试undefined输入 try { signatureAlgorithmHashFromCertificate(undefined, 0) @@ -80,4 +87,4 @@ suite.testAsync('certificate functions edge cases', async () => { } catch (err) { assert(err instanceof Error) } -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-node/test/unit/crypto/crypto-legacy-tests.js b/packages/gaussdb-node/test/unit/crypto/crypto-legacy-tests.js index f74b738d0..74af0833a 100644 --- a/packages/gaussdb-node/test/unit/crypto/crypto-legacy-tests.js +++ b/packages/gaussdb-node/test/unit/crypto/crypto-legacy-tests.js @@ -7,7 +7,7 @@ const suite = new helper.Suite() suite.testAsync('legacy implementation tests', async () => { // 直接测试legacy实现 const crypto = require('../../../lib/crypto/utils-legacy') - + // 检查模块是否导出了必要的函数 assert.strictEqual(typeof crypto.gaussdbMd5PasswordHash, 'function') assert.strictEqual(typeof crypto.gaussdbSha256PasswordHash, 'function') @@ -21,12 +21,12 @@ suite.testAsync('legacy implementation tests', async () => { suite.testAsync('legacy gaussdbMd5PasswordHash function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试MD5密码哈希功能 const user = 'testuser' const password = 'testpass' const salt = Buffer.from([1, 2, 3, 4]) - + const hashed = crypto.gaussdbMd5PasswordHash(user, password, salt) assert.strictEqual(typeof hashed, 'string') assert(hashed.startsWith('md5')) @@ -35,11 +35,11 @@ suite.testAsync('legacy gaussdbMd5PasswordHash function', async () => { suite.testAsync('legacy gaussdbSha256PasswordHash function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试SHA256密码哈希功能 const user = 'testuser' const password = 'testpass' - + // 构造模拟的salt数据,符合GaussDB SHA256认证的数据结构 // 结构: [4 bytes method][64 bytes random code][8 bytes token][4 bytes iteration] const data = Buffer.alloc(80) @@ -47,41 +47,41 @@ suite.testAsync('legacy gaussdbSha256PasswordHash function', async () => { data.write('A'.repeat(64), 4, 'ascii') // 64-byte random code data.write('B'.repeat(8), 68, 'ascii') // 8-byte token data.writeInt32BE(1000, 76) // iteration count - + const hashed = crypto.gaussdbSha256PasswordHash(user, password, data) assert.strictEqual(typeof hashed, 'string') }) suite.testAsync('legacy randomBytes function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试随机字节生成功能 const bytes1 = crypto.randomBytes(16) const bytes2 = crypto.randomBytes(16) - + assert(bytes1 instanceof Buffer) assert.strictEqual(bytes1.length, 16) assert(bytes2 instanceof Buffer) assert.strictEqual(bytes2.length, 16) - + // 两次生成的随机数据应该不同 assert.notStrictEqual(bytes1.toString('hex'), bytes2.toString('hex')) }) suite.testAsync('legacy sha256 function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试SHA256哈希功能 const data = Buffer.from('hello world') const hash = crypto.sha256(data) - + assert(hash instanceof Buffer) assert.strictEqual(hash.length, 32) // SHA256 produces 32-byte output }) suite.testAsync('legacy md5 function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试MD5哈希功能 const hash = crypto.md5('hello world') assert.strictEqual(typeof hash, 'string') @@ -91,12 +91,12 @@ suite.testAsync('legacy md5 function', async () => { suite.testAsync('legacy deriveKey function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试密钥派生功能 const password = 'password' const salt = Buffer.from('salt') const iterations = 100 - + const key = await crypto.deriveKey(password, salt, iterations) // nodeCrypto.pbkdf2Sync返回Buffer assert(key instanceof Buffer) @@ -105,15 +105,15 @@ suite.testAsync('legacy deriveKey function', async () => { suite.testAsync('legacy hashByName function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试hashByName函数 const data = Buffer.from('hello world') - + // 测试SHA-256 const sha256Hash = crypto.hashByName('sha256', data) assert(sha256Hash instanceof Buffer) assert.strictEqual(sha256Hash.length, 32) - + // 测试SHA-1 const sha1Hash = crypto.hashByName('sha1', data) assert(sha1Hash instanceof Buffer) @@ -122,12 +122,12 @@ suite.testAsync('legacy hashByName function', async () => { suite.testAsync('legacy hmacSha256 function', async () => { const crypto = require('../../../lib/crypto/utils-legacy') - + // 测试hmacSha256函数 const key = Buffer.from('key') const message = 'message' - + const hmac = crypto.hmacSha256(key, message) assert(hmac instanceof Buffer) assert.strictEqual(hmac.length, 32) // SHA-256 HMAC produces 32-byte output -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-node/test/unit/crypto/crypto-rfc5802-tests.js b/packages/gaussdb-node/test/unit/crypto/crypto-rfc5802-tests.js index 5900b5cfe..87050825d 100644 --- a/packages/gaussdb-node/test/unit/crypto/crypto-rfc5802-tests.js +++ b/packages/gaussdb-node/test/unit/crypto/crypto-rfc5802-tests.js @@ -6,7 +6,7 @@ const suite = new helper.Suite() // 测试RFC5802算法的详细实现 suite.testAsync('RFC5802Algorithm basic functionality', async () => { const { RFC5802Algorithm } = require('../../../lib/crypto/rfc5802') - + // 测试基本功能 const password = 'password' const random64code = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' // 64字符 @@ -14,7 +14,7 @@ suite.testAsync('RFC5802Algorithm basic functionality', async () => { const serverSignature = '' const serverIteration = 4096 const method = 'sha256' - + const result = RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, method) assert(result instanceof Buffer) assert(result.length > 0) @@ -22,7 +22,7 @@ suite.testAsync('RFC5802Algorithm basic functionality', async () => { suite.testAsync('RFC5802Algorithm with empty password', async () => { const { RFC5802Algorithm } = require('../../../lib/crypto/rfc5802') - + // 测试空密码 const password = '' const random64code = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' @@ -30,7 +30,7 @@ suite.testAsync('RFC5802Algorithm with empty password', async () => { const serverSignature = '' const serverIteration = 4096 const method = 'sha256' - + const result = RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, method) assert(result instanceof Buffer) assert(result.length > 0) @@ -38,7 +38,7 @@ suite.testAsync('RFC5802Algorithm with empty password', async () => { suite.testAsync('RFC5802Algorithm with special characters', async () => { const { RFC5802Algorithm } = require('../../../lib/crypto/rfc5802') - + // 测试特殊字符密码 const password = 'p@ssw0rd!#$%' const random64code = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' @@ -46,7 +46,7 @@ suite.testAsync('RFC5802Algorithm with special characters', async () => { const serverSignature = '' const serverIteration = 4096 const method = 'sha256' - + const result = RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, method) assert(result instanceof Buffer) assert(result.length > 0) @@ -54,7 +54,7 @@ suite.testAsync('RFC5802Algorithm with special characters', async () => { suite.testAsync('RFC5802Algorithm with server signature validation', async () => { const { RFC5802Algorithm } = require('../../../lib/crypto/rfc5802') - + // 测试服务器签名验证 const password = 'password' const random64code = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' @@ -62,7 +62,7 @@ suite.testAsync('RFC5802Algorithm with server signature validation', async () => const serverSignature = 'invalid_signature' const serverIteration = 4096 const method = 'sha256' - + const result = RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, method) assert(result instanceof Buffer) assert.strictEqual(result.length, 0) // 应该返回空缓冲区,因为签名不匹配 @@ -70,7 +70,7 @@ suite.testAsync('RFC5802Algorithm with server signature validation', async () => suite.testAsync('RFC5802Algorithm with different methods', async () => { const { RFC5802Algorithm } = require('../../../lib/crypto/rfc5802') - + // 测试SHA256方法 const password = 'password' const random64code = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' @@ -78,10 +78,10 @@ suite.testAsync('RFC5802Algorithm with different methods', async () => { const serverSignature = '' const serverIteration = 4096 const method = 'sha256' - + const result = RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, method) assert(result instanceof Buffer) - + // 测试不支持的方法 try { RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, 'md5') @@ -89,4 +89,4 @@ suite.testAsync('RFC5802Algorithm with different methods', async () => { } catch (err) { assert.strictEqual(err.message, 'Only sha256 method is supported') } -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-node/test/unit/crypto/crypto-sasl-tests.js b/packages/gaussdb-node/test/unit/crypto/crypto-sasl-tests.js index 8c5402444..99b04d473 100644 --- a/packages/gaussdb-node/test/unit/crypto/crypto-sasl-tests.js +++ b/packages/gaussdb-node/test/unit/crypto/crypto-sasl-tests.js @@ -6,17 +6,17 @@ const suite = new helper.Suite() // 测试SASL功能 suite.testAsync('sasl startSession function', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试基本功能 const mechanisms = ['SCRAM-SHA-256'] const session = sasl.startSession(mechanisms) - + assert.strictEqual(typeof session, 'object') assert.strictEqual(typeof session.mechanism, 'string') assert.strictEqual(typeof session.clientNonce, 'string') assert.strictEqual(typeof session.response, 'string') assert.strictEqual(typeof session.message, 'string') - + assert.strictEqual(session.mechanism, 'SCRAM-SHA-256') assert(session.clientNonce.length > 0) assert(session.response.length > 0) @@ -25,34 +25,34 @@ suite.testAsync('sasl startSession function', async () => { suite.testAsync('sasl startSession with stream', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试带流的会话启动 const mechanisms = ['SCRAM-SHA-256'] const mockStream = {} const session = sasl.startSession(mechanisms, mockStream) - + assert.strictEqual(session.mechanism, 'SCRAM-SHA-256') }) suite.testAsync('sasl startSession with SCRAM-SHA-256-PLUS support', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试SCRAM-SHA-256-PLUS支持 const mechanisms = ['SCRAM-SHA-256', 'SCRAM-SHA-256-PLUS'] const mockStream = { - getPeerCertificate: function() { + getPeerCertificate: function () { return {} - } + }, } const session = sasl.startSession(mechanisms, mockStream) - + // 应该优先选择SCRAM-SHA-256-PLUS assert.strictEqual(session.mechanism, 'SCRAM-SHA-256-PLUS') }) suite.testAsync('sasl startSession error handling', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试不支持的机制 try { sasl.startSession(['UNSUPPORTED-MECH']) @@ -61,7 +61,7 @@ suite.testAsync('sasl startSession error handling', async () => { assert(err instanceof Error) assert(err.message.includes('Only mechanism(s)')) } - + // 测试SCRAM-SHA-256-PLUS需要证书的情况 try { sasl.startSession(['SCRAM-SHA-256-PLUS'], {}) @@ -74,16 +74,16 @@ suite.testAsync('sasl startSession error handling', async () => { suite.testAsync('sasl continueSession function', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试会话继续功能 const session = { message: 'SASLInitialResponse', - clientNonce: 'rOprNGfwEbeRWgbNEkqO' + clientNonce: 'rOprNGfwEbeRWgbNEkqO', } - + const password = 'password' const serverData = 'r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096' - + try { // 这会抛出错误,因为我们没有提供流 await sasl.continueSession(session, password, serverData) @@ -95,12 +95,12 @@ suite.testAsync('sasl continueSession function', async () => { suite.testAsync('sasl continueSession error handling', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试错误处理 - 错误的会话消息 const session = { - message: 'InvalidMessage' + message: 'InvalidMessage', } - + try { await sasl.continueSession(session, 'password', 'serverData') assert.fail('Should have thrown an error for invalid session message') @@ -108,7 +108,7 @@ suite.testAsync('sasl continueSession error handling', async () => { assert(err instanceof Error) assert(err.message.includes('Last message was not SASLInitialResponse')) } - + // 测试错误处理 - 非字符串密码 try { await sasl.continueSession({ message: 'SASLInitialResponse' }, 123, 'serverData') @@ -117,7 +117,7 @@ suite.testAsync('sasl continueSession error handling', async () => { assert(err instanceof Error) assert(err.message.includes('client password must be a string')) } - + // 测试错误处理 - 空密码 try { await sasl.continueSession({ message: 'SASLInitialResponse' }, '', 'serverData') @@ -126,7 +126,7 @@ suite.testAsync('sasl continueSession error handling', async () => { assert(err instanceof Error) assert(err.message.includes('client password must be a non-empty string')) } - + // 测试错误处理 - 非字符串服务器数据 try { await sasl.continueSession({ message: 'SASLInitialResponse' }, 'password', 123) @@ -139,15 +139,15 @@ suite.testAsync('sasl continueSession error handling', async () => { suite.testAsync('sasl finalizeSession function', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试会话结束功能 const session = { salt: 'salt', - serverSignature: 'signature' + serverSignature: 'signature', } - + const serverData = 'v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=' - + try { sasl.finalizeSession(session, serverData) // 如果没有抛出错误,说明函数执行成功 @@ -159,16 +159,16 @@ suite.testAsync('sasl finalizeSession function', async () => { suite.testAsync('sasl parseServerFirstMessage function', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 通过间接方式测试parseServerFirstMessage函数 const session = { message: 'SASLInitialResponse', - clientNonce: 'rOprNGfwEbeRWgbNEkqO' + clientNonce: 'rOprNGfwEbeRWgbNEkqO', } - + // 有效的服务器数据 const serverData = 'r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096' - + try { // 这个调用会间接测试parseServerFirstMessage函数 await sasl.continueSession(session, 'password', serverData) @@ -176,10 +176,10 @@ suite.testAsync('sasl parseServerFirstMessage function', async () => { // 错误是预期的,因为我们没有提供流 assert(err instanceof Error) } - + // 无效的服务器数据 const invalidServerData = 'invalid,data' - + try { await sasl.continueSession(session, 'password', invalidServerData) assert.fail('Should have thrown an error for invalid server data') @@ -191,7 +191,7 @@ suite.testAsync('sasl parseServerFirstMessage function', async () => { // 测试边界情况 suite.testAsync('sasl edge cases', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试空机制数组 try { sasl.startSession([]) @@ -200,11 +200,11 @@ suite.testAsync('sasl edge cases', async () => { assert(err instanceof Error) assert(err.message.includes('Only mechanism(s)')) } - + // 测试随机数生成功能 const session1 = sasl.startSession(['SCRAM-SHA-256']) const session2 = sasl.startSession(['SCRAM-SHA-256']) - + // 两次生成的随机数应该不同 assert.notStrictEqual(session1.clientNonce, session2.clientNonce) -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-node/test/unit/crypto/crypto-tests.js b/packages/gaussdb-node/test/unit/crypto/crypto-tests.js index d192fbf10..cd6dc65f6 100644 --- a/packages/gaussdb-node/test/unit/crypto/crypto-tests.js +++ b/packages/gaussdb-node/test/unit/crypto/crypto-tests.js @@ -6,7 +6,7 @@ const suite = new helper.Suite() // 测试加密模块的核心功能 suite.testAsync('crypto module exports', async () => { const crypto = require('../../../lib/crypto/utils') - + // 检查模块是否导出了必要的函数 assert.strictEqual(typeof crypto.gaussdbMd5PasswordHash, 'function') assert.strictEqual(typeof crypto.gaussdbSha256PasswordHash, 'function') @@ -18,12 +18,12 @@ suite.testAsync('crypto module exports', async () => { suite.testAsync('gaussdbMd5PasswordHash function', async () => { const crypto = require('../../../lib/crypto/utils') - + // 测试MD5密码哈希功能 const user = 'testuser' const password = 'testpass' const salt = Buffer.from([1, 2, 3, 4]) - + const hashed = await crypto.gaussdbMd5PasswordHash(user, password, salt) assert.strictEqual(typeof hashed, 'string') assert(hashed.startsWith('md5')) @@ -32,11 +32,11 @@ suite.testAsync('gaussdbMd5PasswordHash function', async () => { suite.testAsync('gaussdbSha256PasswordHash function', async () => { const crypto = require('../../../lib/crypto/utils') - + // 测试SHA256密码哈希功能 const user = 'testuser' const password = 'testpass' - + // 构造模拟的salt数据,符合GaussDB SHA256认证的数据结构 // 结构: [4 bytes method][64 bytes random code][8 bytes token][4 bytes iteration] const data = Buffer.alloc(80) @@ -44,7 +44,7 @@ suite.testAsync('gaussdbSha256PasswordHash function', async () => { data.write('A'.repeat(64), 4, 'ascii') // 64-byte random code data.write('B'.repeat(8), 68, 'ascii') // 8-byte token data.writeInt32BE(1000, 76) // iteration count - + const hashed = await crypto.gaussdbSha256PasswordHash(user, password, data) assert.strictEqual(typeof hashed, 'string') // SHA256哈希结果应该是ASCII字符串 @@ -52,27 +52,27 @@ suite.testAsync('gaussdbSha256PasswordHash function', async () => { suite.testAsync('randomBytes function', async () => { const crypto = require('../../../lib/crypto/utils') - + // 测试随机字节生成功能 const bytes1 = crypto.randomBytes(16) const bytes2 = crypto.randomBytes(16) - + assert(bytes1 instanceof Buffer) assert.strictEqual(bytes1.length, 16) assert(bytes2 instanceof Buffer) assert.strictEqual(bytes2.length, 16) - + // 两次生成的随机数据应该不同 assert.notStrictEqual(bytes1.toString('hex'), bytes2.toString('hex')) }) suite.testAsync('sha256 function', async () => { const crypto = require('../../../lib/crypto/utils') - + // 测试SHA256哈希功能 const data = Buffer.from('hello world') const hash = await crypto.sha256(data) - + // WebCrypto实现返回ArrayBuffer而不是Buffer assert(hash instanceof ArrayBuffer || hash instanceof Buffer) assert.strictEqual(hash.byteLength || hash.length, 32) // SHA256 produces 32-byte output @@ -80,7 +80,7 @@ suite.testAsync('sha256 function', async () => { suite.testAsync('md5 function', async () => { const crypto = require('../../../lib/crypto/utils') - + // 测试MD5哈希功能 const hash = await crypto.md5('hello world') assert.strictEqual(typeof hash, 'string') @@ -90,12 +90,12 @@ suite.testAsync('md5 function', async () => { suite.testAsync('deriveKey function', async () => { const crypto = require('../../../lib/crypto/utils') - + // 测试密钥派生功能 const password = 'password' const salt = Buffer.from('salt') const iterations = 100 - + const key = await crypto.deriveKey(password, salt, iterations) // 不同实现返回不同类型,WebCrypto返回ArrayBuffer,Legacy返回Buffer assert(key instanceof ArrayBuffer || key instanceof Buffer) @@ -105,7 +105,7 @@ suite.testAsync('deriveKey function', async () => { // 测试RFC5802算法实现 suite.testAsync('RFC5802Algorithm function', async () => { const { RFC5802Algorithm } = require('../../../lib/crypto/rfc5802') - + // 测试RFC5802算法实现 const password = 'password' const random64code = 'A'.repeat(64) @@ -113,7 +113,7 @@ suite.testAsync('RFC5802Algorithm function', async () => { const serverSignature = '' const serverIteration = 1000 const method = 'sha256' - + const result = RFC5802Algorithm(password, random64code, token, serverSignature, serverIteration, method) assert(result instanceof Buffer) }) @@ -121,7 +121,7 @@ suite.testAsync('RFC5802Algorithm function', async () => { // 测试证书签名功能 suite.testAsync('signatureAlgorithmHashFromCertificate function', async () => { const { signatureAlgorithmHashFromCertificate } = require('../../../lib/crypto/cert-signatures') - + // 测试证书签名算法哈希功能(简单测试) assert.strictEqual(typeof signatureAlgorithmHashFromCertificate, 'function') }) @@ -129,21 +129,21 @@ suite.testAsync('signatureAlgorithmHashFromCertificate function', async () => { // 测试SASL功能 suite.testAsync('SASL functions', async () => { const sasl = require('../../../lib/crypto/sasl') - + // 测试SASL会话启动功能 assert.strictEqual(typeof sasl.startSession, 'function') assert.strictEqual(typeof sasl.continueSession, 'function') assert.strictEqual(typeof sasl.finalizeSession, 'function') - + // 测试startSession函数 const mechanisms = ['SCRAM-SHA-256'] const session = sasl.startSession(mechanisms) - + assert.strictEqual(typeof session.mechanism, 'string') assert.strictEqual(typeof session.clientNonce, 'string') assert.strictEqual(typeof session.response, 'string') assert.strictEqual(typeof session.message, 'string') - + // 测试错误情况 - 不支持的机制 try { sasl.startSession(['UNSUPPORTED-MECH']) @@ -152,4 +152,4 @@ suite.testAsync('SASL functions', async () => { // 错误消息可能因环境而异,我们只检查它是否是Error实例 assert(err instanceof Error) } -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-node/test/unit/crypto/crypto-webcrypto-tests.js b/packages/gaussdb-node/test/unit/crypto/crypto-webcrypto-tests.js index d9edf271a..056370f43 100644 --- a/packages/gaussdb-node/test/unit/crypto/crypto-webcrypto-tests.js +++ b/packages/gaussdb-node/test/unit/crypto/crypto-webcrypto-tests.js @@ -7,7 +7,7 @@ const suite = new helper.Suite() suite.testAsync('webcrypto implementation tests', async () => { // 直接测试webcrypto实现 const crypto = require('../../../lib/crypto/utils-webcrypto') - + // 检查模块是否导出了必要的函数 assert.strictEqual(typeof crypto.gaussdbMd5PasswordHash, 'function') assert.strictEqual(typeof crypto.gaussdbSha256PasswordHash, 'function') @@ -21,25 +21,25 @@ suite.testAsync('webcrypto implementation tests', async () => { suite.testAsync('webcrypto gaussdbSha256PasswordHash with various inputs', async () => { const crypto = require('../../../lib/crypto/utils-webcrypto') - + // 测试不同的输入参数 const user = 'testuser' const password = 'testpass' - + // 测试正常输入 const data1 = Buffer.alloc(80) data1.writeInt32BE(1, 0) data1.write('A'.repeat(64), 4, 'ascii') data1.write('B'.repeat(8), 68, 'ascii') data1.writeInt32BE(4096, 76) - + const result1 = await crypto.gaussdbSha256PasswordHash(user, password, data1) assert.strictEqual(typeof result1, 'string') - + // 测试空密码 const result2 = await crypto.gaussdbSha256PasswordHash(user, '', data1) assert.strictEqual(typeof result2, 'string') - + // 测试特殊字符密码 const result3 = await crypto.gaussdbSha256PasswordHash(user, 'p@ssw0rd!#$', data1) assert.strictEqual(typeof result3, 'string') @@ -47,16 +47,16 @@ suite.testAsync('webcrypto gaussdbSha256PasswordHash with various inputs', async suite.testAsync('webcrypto hashByName function', async () => { const crypto = require('../../../lib/crypto/utils-webcrypto') - + // 测试hashByName函数 const data = Buffer.from('hello world') - + // 测试SHA-256 const sha256Hash = await crypto.hashByName('SHA-256', data) // WebCrypto返回ArrayBuffer而不是Buffer assert(sha256Hash instanceof ArrayBuffer) assert.strictEqual(sha256Hash.byteLength, 32) - + // 测试SHA-1 const sha1Hash = await crypto.hashByName('SHA-1', data) assert(sha1Hash instanceof ArrayBuffer) @@ -65,11 +65,11 @@ suite.testAsync('webcrypto hashByName function', async () => { suite.testAsync('webcrypto hmacSha256 function', async () => { const crypto = require('../../../lib/crypto/utils-webcrypto') - + // 测试hmacSha256函数 const key = Buffer.from('key') const message = 'message' - + const hmac = await crypto.hmacSha256(key, message) // WebCrypto的hmacSha256返回ArrayBuffer而不是Buffer assert(hmac instanceof ArrayBuffer) @@ -78,12 +78,12 @@ suite.testAsync('webcrypto hmacSha256 function', async () => { suite.testAsync('webcrypto deriveKey function', async () => { const crypto = require('../../../lib/crypto/utils-webcrypto') - + // 测试密钥派生函数 const password = 'password' const salt = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) const iterations = 1000 - + const key = await crypto.deriveKey(password, salt, iterations) assert(key instanceof ArrayBuffer) assert.strictEqual(key.byteLength, 32) // Should be 32 bytes for SHA-256 @@ -91,15 +91,15 @@ suite.testAsync('webcrypto deriveKey function', async () => { suite.testAsync('webcrypto edge cases', async () => { const crypto = require('../../../lib/crypto/utils-webcrypto') - + // 测试边界情况 // 1. 空输入 const emptyHash = await crypto.md5('') assert.strictEqual(typeof emptyHash, 'string') - + // 2. 长输入 const longString = 'a'.repeat(10000) const longHash = await crypto.md5(longString) assert.strictEqual(typeof longHash, 'string') assert.strictEqual(longHash.length, 32) -}) \ No newline at end of file +}) diff --git a/packages/gaussdb-protocol/src/buffer-reader.ts b/packages/gaussdb-protocol/src/buffer-reader.ts index 62b16a2ed..fcf63e642 100644 --- a/packages/gaussdb-protocol/src/buffer-reader.ts +++ b/packages/gaussdb-protocol/src/buffer-reader.ts @@ -1,12 +1,17 @@ const emptyBuffer = Buffer.allocUnsafe(0) +export type Endian = 'be' | 'le' + export class BufferReader { private buffer: Buffer = emptyBuffer // TODO(bmc): support non-utf8 encoding? private encoding: string = 'utf-8' - constructor(private offset: number = 0) {} + constructor( + private offset: number = 0, + private endian: Endian = 'be' + ) {} public setBuffer(offset: number, buffer: Buffer): void { this.offset = offset @@ -14,7 +19,13 @@ export class BufferReader { } public int16(): number { - const result = this.buffer.readInt16BE(this.offset) + const result = this.endian === 'le' ? this.buffer.readInt16LE(this.offset) : this.buffer.readInt16BE(this.offset) + this.offset += 2 + return result + } + + public uint16(): number { + const result = this.endian === 'le' ? this.buffer.readUInt16LE(this.offset) : this.buffer.readUInt16BE(this.offset) this.offset += 2 return result } @@ -25,16 +36,38 @@ export class BufferReader { return result } + public peekByte(): number { + return this.buffer[this.offset] + } + public int32(): number { - const result = this.buffer.readInt32BE(this.offset) + const result = this.endian === 'le' ? this.buffer.readInt32LE(this.offset) : this.buffer.readInt32BE(this.offset) this.offset += 4 return result } public uint32(): number { - const result = this.buffer.readUInt32BE(this.offset) + const result = this.endian === 'le' ? this.buffer.readUInt32LE(this.offset) : this.buffer.readUInt32BE(this.offset) this.offset += 4 - return result + return result >>> 0 + } + + public uint64Parts(): { hi: number; lo: number } { + let hi: number + let lo: number + if (this.endian === 'le') { + lo = this.buffer.readUInt32LE(this.offset) + hi = this.buffer.readUInt32LE(this.offset + 4) + } else { + hi = this.buffer.readUInt32BE(this.offset) + lo = this.buffer.readUInt32BE(this.offset + 4) + } + this.offset += 8 + return { hi: hi >>> 0, lo: lo >>> 0 } + } + + public remaining(): number { + return this.buffer.length - this.offset } public string(length: number): string { diff --git a/packages/gaussdb-protocol/src/index.ts b/packages/gaussdb-protocol/src/index.ts index 703ff2e49..4a9c8dde7 100644 --- a/packages/gaussdb-protocol/src/index.ts +++ b/packages/gaussdb-protocol/src/index.ts @@ -1,6 +1,7 @@ import { DatabaseError } from './messages' import { serialize } from './serializer' import { Parser, MessageCallback } from './parser' +import { BufferReader, Endian } from './buffer-reader' export function parse(stream: NodeJS.ReadableStream, callback: MessageCallback): Promise { const parser = new Parser() @@ -8,4 +9,5 @@ export function parse(stream: NodeJS.ReadableStream, callback: MessageCallback): return new Promise((resolve) => stream.on('end', () => resolve())) } -export { serialize, DatabaseError } +export { serialize, DatabaseError, BufferReader } +export type { Endian }