Skip to content

Commit 529402b

Browse files
fix: rework FeatureExt generation (#1718)
1 parent cfb211a commit 529402b

File tree

2 files changed

+190
-8
lines changed

2 files changed

+190
-8
lines changed

src/login7-payload.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,16 +251,24 @@ class Login7Payload {
251251
}
252252

253253
// (ibUnused / ibExtension): 2-byte
254+
// For TDS 7.4+, this points to a 4-byte offset (ibFeatureExtLong) in the data section.
255+
// The actual FeatureExt data is placed at the END of the packet per MS-TDS spec.
254256
offset = fixedData.writeUInt16LE(dataOffset, offset);
255257

256258
// (cchUnused / cbExtension): 2-byte
259+
// For TDS 7.4+, this is the size of the ibFeatureExtLong offset pointer (4 bytes).
260+
// The actual FeatureExt data is appended at the end of the packet, not here.
261+
// We'll store the FeatureExt data to append at the end after all other variable data.
262+
let featureExtData: Buffer | undefined;
263+
let extensionOffsetBuffer: Buffer | undefined;
257264
if (this.tdsVersion >= versions['7_4']) {
258-
const extensions = this.buildFeatureExt();
259-
offset = fixedData.writeUInt16LE(4 + extensions.length, offset);
260-
const extensionOffset = Buffer.alloc(4);
261-
extensionOffset.writeUInt32LE(dataOffset + 4, 0);
262-
dataOffset += 4 + extensions.length;
263-
buffers.push(extensionOffset, extensions);
265+
featureExtData = this.buildFeatureExt();
266+
// cbExtension = 4 (size of the ibFeatureExtLong pointer, not the FeatureExt data)
267+
offset = fixedData.writeUInt16LE(4, offset);
268+
// Reserve space for the 4-byte offset pointer; we'll fill in the actual offset later
269+
extensionOffsetBuffer = Buffer.alloc(4);
270+
buffers.push(extensionOffsetBuffer);
271+
dataOffset += 4;
264272
} else {
265273
// For TDS < 7.4, these are unused fields
266274
offset = fixedData.writeUInt16LE(0, offset);
@@ -329,6 +337,7 @@ class Login7Payload {
329337
}
330338

331339
buffers.push(this.sspi);
340+
dataOffset += this.sspi.length;
332341
} else {
333342
offset = fixedData.writeUInt16LE(0, offset);
334343
}
@@ -370,6 +379,15 @@ class Login7Payload {
370379
fixedData.writeUInt32LE(0, offset);
371380
}
372381

382+
// Per MS-TDS spec, FeatureExt data must be at the END of the packet,
383+
// after all other variable-length data.
384+
if (featureExtData && extensionOffsetBuffer) {
385+
// Update the ibFeatureExtLong offset to point to where FeatureExt will be
386+
extensionOffsetBuffer.writeUInt32LE(dataOffset, 0);
387+
// Append FeatureExt data at the end
388+
buffers.push(featureExtData);
389+
}
390+
373391
const data = Buffer.concat(buffers);
374392
data.writeUInt32LE(data.length, 0);
375393
return data;

test/unit/login7-payload-test.ts

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,7 @@ describe('Login7Payload', function() {
246246
connectionId: 0,
247247
clientTimeZone: 120,
248248
clientLcid: 0x00000409,
249-
isFabric: true,
250-
} as any);
249+
});
251250

252251
payload.hostname = 'example.com';
253252
payload.appName = 'app';
@@ -288,5 +287,170 @@ describe('Login7Payload', function() {
288287
assert.lengthOf(data, expectedLength);
289288
});
290289
});
290+
291+
describe('FeatureExt positioning per MS-TDS spec', function() {
292+
it('places ibFeatureExtLong offset pointer at the correct position', function() {
293+
const payload = new Login7Payload({
294+
tdsVersion: 0x74000004,
295+
packetSize: 1024,
296+
clientProgVer: 0,
297+
clientPid: 12345,
298+
connectionId: 0,
299+
clientTimeZone: 120,
300+
clientLcid: 0x00000409,
301+
});
302+
303+
payload.hostname = 'testhost';
304+
payload.userName = 'testuser';
305+
payload.password = 'testpass';
306+
payload.appName = 'TestApp';
307+
payload.serverName = 'testserver';
308+
payload.language = 'us_english';
309+
payload.database = 'master';
310+
payload.libraryName = 'Tedious';
311+
payload.fedAuth = {
312+
type: 'ADAL',
313+
echo: true,
314+
workflow: 'default'
315+
};
316+
317+
const data = payload.toBuffer();
318+
319+
// ibExtension is at fixed offset 56 (2 bytes for offset, 2 bytes for length)
320+
const ibExtension = data.readUInt16LE(56);
321+
const cbExtension = data.readUInt16LE(58);
322+
323+
// cbExtension should be 4 (the size of the ibFeatureExtLong pointer)
324+
assert.strictEqual(cbExtension, 4, 'cbExtension should be 4 bytes (ibFeatureExtLong pointer size)');
325+
326+
// Read the ibFeatureExtLong value (4-byte offset to FeatureExt data)
327+
const ibFeatureExtLong = data.readUInt32LE(ibExtension);
328+
329+
// Verify ibFeatureExtLong points to a valid position in the packet
330+
assert.isAtLeast(ibFeatureExtLong, 94, 'ibFeatureExtLong should point past the fixed header');
331+
assert.isBelow(ibFeatureExtLong, data.length, 'ibFeatureExtLong should point within the packet');
332+
333+
// Verify FeatureExt data starts at the position pointed to by ibFeatureExtLong
334+
// First feature should be FEDAUTH (0x02) or UTF8_SUPPORT (0x0A)
335+
const firstFeatureId = data.readUInt8(ibFeatureExtLong);
336+
assert.oneOf(firstFeatureId, [0x02, 0x0A], 'First feature should be FEDAUTH (0x02) or UTF8_SUPPORT (0x0A)');
337+
338+
// Find all variable data fields to verify FeatureExt is at the END
339+
// Fixed header layout (after ClientLCID at offset 32-35):
340+
// 36-39: ibHostName/cchHostName, 40-43: ibUserName/cchUserName
341+
// 44-47: ibPassword/cchPassword, 48-51: ibAppName/cchAppName
342+
// 52-55: ibServerName/cchServerName, 56-59: ibExtension/cbExtension
343+
// 60-63: ibCltIntName/cchCltIntName, 64-67: ibLanguage/cchLanguage
344+
// 68-71: ibDatabase/cchDatabase, 72-77: ClientID (6 bytes)
345+
// 78-81: ibSSPI/cbSSPI, 82-85: ibAtchDBFile/cchAtchDBFile
346+
// 86-89: ibChangePassword/cchChangePassword, 90-93: cbSSPILong
347+
const ibHostName = data.readUInt16LE(36);
348+
const cchHostName = data.readUInt16LE(38);
349+
const ibUserName = data.readUInt16LE(40);
350+
const cchUserName = data.readUInt16LE(42);
351+
const ibPassword = data.readUInt16LE(44);
352+
const cchPassword = data.readUInt16LE(46);
353+
const ibAppName = data.readUInt16LE(48);
354+
const cchAppName = data.readUInt16LE(50);
355+
const ibServerName = data.readUInt16LE(52);
356+
const cchServerName = data.readUInt16LE(54);
357+
const ibCltIntName = data.readUInt16LE(60);
358+
const cchCltIntName = data.readUInt16LE(62);
359+
const ibLanguage = data.readUInt16LE(64);
360+
const cchLanguage = data.readUInt16LE(66);
361+
const ibDatabase = data.readUInt16LE(68);
362+
const cchDatabase = data.readUInt16LE(70);
363+
const ibSSPI = data.readUInt16LE(78);
364+
const cbSSPI = data.readUInt16LE(80);
365+
const ibAttachDBFile = data.readUInt16LE(82);
366+
const cchAttachDBFile = data.readUInt16LE(84);
367+
const ibChangePassword = data.readUInt16LE(86);
368+
const cchChangePassword = data.readUInt16LE(88);
369+
370+
// Calculate the end of all regular variable data (excluding FeatureExt)
371+
// Include all variable-length fields to ensure complete coverage
372+
const variableDataEnd = Math.max(
373+
ibHostName + cchHostName * 2,
374+
ibUserName + cchUserName * 2,
375+
ibPassword + cchPassword * 2,
376+
ibAppName + cchAppName * 2,
377+
ibServerName + cchServerName * 2,
378+
ibCltIntName + cchCltIntName * 2,
379+
ibLanguage + cchLanguage * 2,
380+
ibDatabase + cchDatabase * 2,
381+
ibSSPI + cbSSPI,
382+
ibAttachDBFile + cchAttachDBFile * 2,
383+
ibChangePassword + cchChangePassword * 2
384+
);
385+
386+
// FeatureExt should start after all other variable data
387+
assert.isAtLeast(ibFeatureExtLong, variableDataEnd,
388+
'FeatureExt (at ' + ibFeatureExtLong + ') should be after all variable data (ends at ' + variableDataEnd + ')');
389+
390+
// Verify FeatureExt ends with terminator (0xFF)
391+
// Find the terminator by scanning from ibFeatureExtLong
392+
let featureOffset = ibFeatureExtLong;
393+
while (featureOffset < data.length) {
394+
const featureId = data.readUInt8(featureOffset);
395+
if (featureId === 0xFF) {
396+
// Found terminator, verify it's at the end of the packet
397+
assert.strictEqual(featureOffset, data.length - 1,
398+
'FeatureExt terminator should be the last byte of the packet');
399+
break;
400+
}
401+
// Skip past this feature: 1 byte ID + 4 bytes length + length bytes data
402+
const featureLen = data.readUInt32LE(featureOffset + 1);
403+
featureOffset += 1 + 4 + featureLen;
404+
}
405+
});
406+
407+
it('correctly positions FeatureExt when SSPI data is present', function() {
408+
const payload = new Login7Payload({
409+
tdsVersion: 0x74000004,
410+
packetSize: 1024,
411+
clientProgVer: 0,
412+
clientPid: 12345,
413+
connectionId: 0,
414+
clientTimeZone: 120,
415+
clientLcid: 0x00000409,
416+
});
417+
418+
payload.hostname = 'testhost';
419+
payload.appName = 'TestApp';
420+
payload.serverName = 'testserver';
421+
payload.language = 'us_english';
422+
payload.database = 'master';
423+
payload.libraryName = 'Tedious';
424+
// Add SSPI data to test offset calculation
425+
payload.sspi = Buffer.from([0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00]); // "NTLMSSP\0"
426+
payload.fedAuth = {
427+
type: 'ADAL',
428+
echo: true,
429+
workflow: 'default'
430+
};
431+
432+
const data = payload.toBuffer();
433+
434+
// Read the ibExtension and ibFeatureExtLong
435+
const ibExtension = data.readUInt16LE(56);
436+
const ibFeatureExtLong = data.readUInt32LE(ibExtension);
437+
438+
// ibSSPI is at offset 78, cbSSPI at offset 80 (after ClientID at 72-77)
439+
const ibSSPI = data.readUInt16LE(78);
440+
const cbSSPI = data.readUInt16LE(80);
441+
442+
// SSPI data should be present in the packet
443+
assert.strictEqual(cbSSPI, 8, 'SSPI length should be 8 bytes');
444+
445+
// FeatureExt should be after SSPI data
446+
const sspiEnd = ibSSPI + cbSSPI;
447+
assert.isAtLeast(ibFeatureExtLong, sspiEnd,
448+
'FeatureExt should be after SSPI data');
449+
450+
// Verify first feature is valid
451+
const firstFeatureId = data.readUInt8(ibFeatureExtLong);
452+
assert.oneOf(firstFeatureId, [0x02, 0x0A], 'First feature should be FEDAUTH (0x02) or UTF8_SUPPORT (0x0A)');
453+
});
454+
});
291455
});
292456
});

0 commit comments

Comments
 (0)