@@ -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