require 'rex/proto/drda'

module Rex
module Proto
module DRDA

class Error < StandardError; end
class RespError < Error; end

# See:
# http://publib.boulder.ibm.com/infocenter/dzichelp/v2r2/index.jsp?topic=/com.ibm.db29.doc.drda/db2z_excsat.htm 
class MGRLVLLS_PARAM < Struct.new(:length, :codepoint, :payload)
	def initialize(args={})
		self[:codepoint] = Constants::MGRLVLLS
		self[:payload] = "\x14\x03\x00\x0a\x24\x07\x00\x0a" +
			"\x14\x74\x00\x05\x24\x0f\x00\x08" + 
			"\x14\x40\x00\x09\x1c\x08\x04\xb8"
		self[:length] = self[:payload].to_s.size+4
	end
	def to_s
		self.to_a.pack("nna*")
	end
end

# Currently, only takes a MGRLVLLS param. Extend the struct 
# when more parameters are defined.
class EXCSAT_DDM < Struct.new(:length, :magic, :format, :correlid, :length2,
	 :codepoint, :mgrlvlls) 

	def initialize(args={})
		self[:magic] = 0xd0
		self[:format] = 0x41
		self[:correlid] = 1
		self[:codepoint] = Constants::EXCSAT
		self[:mgrlvlls] = args[:mgrlvlls] || MGRLVLLS_PARAM.new.to_s
		self[:length] = (10 + self[:mgrlvlls].to_s.size)
		self[:length2] = self[:length]-6 
	end

	def to_s
		packstr = "nCCnnn"
		packstr += "a*"  # Pack smarter as more params are added.
		self.to_a.pack(packstr)
	end
end

# See http://publib.boulder.ibm.com/infocenter/dzichelp/v2r2/index.jsp?topic=/com.ibm.db29.doc.drda/db2z_accsec.htm
# for all sorts of info about SECMEC.
class SECMEC_PARAM < Struct.new(:length, :codepoint, :payload)
	def initialize(args={})
		self[:length] = 6
		self[:codepoint] = Constants::SECMEC
		self[:payload] = 3 # Plaintext username and password.
	end
	def to_s
		self.to_a.pack("nnn")
	end
end

# Relational Database name parameter. 
class RDBNAM_PARAM < Struct.new(:length, :codepoint, :payload)
	def initialize(args={})
		self[:length] = 22 # Since the database name is padded out.
		self[:codepoint] = Constants::RDBNAM
		self[:payload] = encode(args[:payload].to_s)
	end

	def encode(str)
		Rex::Text.to_ebcdic([str].pack("A18"))
	end

	def payload=(str)
		self[:payload] = encode(str.to_s)
	end

	def to_s
		self.to_a.pack("nna18")
	end

end

# The ACCSEC DDM is responsible for picking the security mechanism (SECMEC)
# which, in our case, will always be plain text username and password. It
# also sets the relational database name (RDBNAM), if specified. You need
# one to login, but not to probe.
class ACCSEC_DDM < Struct.new(:length, :magic, :format, :correlid, :length2,
	 :codepoint, :secmec, :rdbnam)
	def initialize(args={})
		self[:magic] = 0xd0
		self[:format] = args[:format] || 0x01
		self[:correlid] = 2
		self[:codepoint] = Constants::ACCSEC
		self[:secmec] = SECMEC_PARAM.new.to_s
		if args[:dbname] # Include a database name if we're given one.
			self[:rdbnam] = RDBNAM_PARAM.new(:payload => args[:dbname]).to_s
		end
		self[:length] =  10 + self[:secmec].to_s.size + self[:rdbnam].to_s.size
		self[:length2] = self[:length]-6 
	end
	def dbname=(str)
		self[:rdbnam] = RDBNAM_PARAM.new(:payload => args[:dbname]).to_s
	end
	def to_s
		packstr = "nCCnnna6"
		packstr += "a22" if self[:rdbnam]
		self.to_a.pack(packstr)
	end
end

class DDM_PARAM < Struct.new(:length, :codepoint, :payload)

	def read(str="")
		raise DRDA::Error, "Input isn't a String." if !str.kind_of? String
		raise DRDA::RespError, "DDM_PARAM is too short" if str.size < 4
		(self[:length], self[:codepoint]) =
			str.unpack("nn")
		raise DRDA::RespError, "DDM_PARAM Length is too short" if self[:length] < 4
		rest = str[4,self[:length]-4] # If it's negative or whatever, it'll end up as "".
		self[:payload] = rest.to_s[0,self[:length]-4]
		return self
	end

	def to_s
		self.to_a.pack("nna*")
	end

end

class BASIC_DDM < Struct.new(:length, :magic, :format, :correlid,
	:length2, :codepoint, :payload)
	def initialize
		self[:payload] = []
	end

	def read(str="")
		self[:payload].clear
		raise DRDA::Error, "Input isn't a String." if !str.kind_of? String
		raise DRDA::RespError, "Response is too short." if str.size < 10
		(self[:length],self[:magic],self[:format],
		 self[:correlid],self[:length2],self[:codepoint]) =
		 str.unpack("nCCnnn")
		sanity_check
		rest = str[10,self[:length2]-4]
		i = 0
		while (i < rest.size)
			if self[:codepoint] == Constants::SQLCARD # These aren't DDM's. 
				this_param = rest[i,self[:length]-10]
			else
				this_param = DDM_PARAM.new.read(rest[i,rest.size])
			end
			self[:payload] << this_param
			i += this_param.to_s.size
		end
		return self
	end

	# Just a quick test. 
	def sanity_check
		if self[:length] < 10
			raise DRDA::RespError, "DDM Length is too short."
		elsif self[:length2] < 4
			raise DRDA::RespError, "DDM Length2 is too short."
		elsif self[:length]-6 != self[:length2]
			raise DRDA::RespError, "Codepoint: 0x#{self[:codepoint].to_s(16)} DDM Length2 (0x#{self[:length2].to_s(16)}) isn't six less than Length (0x#{self[:length].to_s(16)})"
		end
	end

	def to_s
		self.to_a.pack("nCCnnn") + self[:payload].map {|x| x.to_s}.join
	end

end

class SERVER_PACKET < Array

	def read(str="")
		raise DRDA::Error, "Input isn't a String." if !str.kind_of? String
		self.clear
		i = 0
		while(i < str.size)
			this_ddm = BASIC_DDM.new.read(str[i,str.size])
			self << this_ddm
			i += this_ddm.to_s.size
		end
		return self
	end

	def to_s; self.join; end
	def sz; self.to_s.size; end

end

class PASSWORD_PARAM < Struct.new(:length, :codepoint, :payload)
	def initialize(args={})
		self[:codepoint] = Constants::PASSWORD
		self[:payload] = Rex::Text.to_ebcdic(args[:payload].to_s)
		self[:length] = self[:payload].size + 4
	end
	def encode(str)
		Rex::Text.to_ebcdic(str)
	end
	def to_s
		self.to_a.pack("nna*")
	end
end

class USERID_PARAM < Struct.new(:length, :codepoint, :payload)
	def initialize(args={})
		self[:codepoint] = Constants::USERID
		self[:payload] = Rex::Text.to_ebcdic(args[:payload].to_s)
		self[:length] = self[:payload].size + 4
	end
	def encode(str)
		Rex::Text.to_ebcdic(str)
	end
	def to_s
		self.to_a.pack("nna*")
	end
end

class SECCHK_DDM < Struct.new(:length, :magic, :format, :correlid, :length2,
	:codepoint, :secmec, :rdbnam, :password, :userid)
	def initialize(args={}) # Takes :dbname, :dbpass, :dbuser
		self[:magic] = 0xd0
		self[:format] = 0x01
		self[:correlid] = 2
		self[:codepoint] = Constants::SECCHK
		self[:secmec] = SECMEC_PARAM.new.to_s
		if args[:dbname] # Include a database name if we're given one.
			self[:rdbnam] = RDBNAM_PARAM.new(:payload => args[:dbname]).to_s
		end
		self[:password] = PASSWORD_PARAM.new(:payload => args[:dbpass]).to_s
		self[:userid] = USERID_PARAM.new(:payload => args[:dbuser]).to_s
		self[:length] = ( 10 + self[:secmec].to_s.size + self[:rdbnam].to_s.size +
					 self[:password].to_s.size + self[:userid].to_s.size )
		self[:length2] = self[:length]-6 
	end
	def dbname=(str)
		self[:rdbnam] = RDBNAM_PARAM.new(:payload => args[:dbname]).to_s
	end
	def to_s
		packstr = "nCCnnna6"
		packstr += "a22" if self[:rdbnam]
		packstr += "a*a*" # username and password
		self.to_a.pack(packstr)
	end
end

end
end
end

