---
-- GIOP Library supporting a very limited subset of operations
--
-- Summary
-- -------
-- 	The library currently provides functionality to connect and query the
--  CORBA naming service for a list of available objects.
--
--
-- Overview
-- --------
-- The library contains the following classes:
--
--   o Packet.*
--		- The Packet classes contain specific packets and function to serialize
--        them to strings that can be sent over the wire. Each class may also
--        contain a function to parse the servers response.
--
--   o Comm
--		- Implements a number of functions to handle communication over the
--        the Socket class.
--
--   o Helper
--		- A helper class that provides easy access to the rest of the library
--
--   o Socket
--      - This is a copy of the DB2Socket class which provides fundamental 
--        buffering
--
--
-- Example
-- -------
-- The following sample code illustrates how scripts can use the Helper class
-- to interface the library:
--
-- <code>
--	helper 	= giop.Helper:new(host, port)
--  status, err = helper:Connect()
--  status, ctx = helper:GetNamingContext()
--  status, objs = helper:ListObjects(ctx)
-- </code>
--
-- Additional information
-- ----------------------
-- The implementation is based on packet dumps and the decoding Wireshark
-- provides.
--
-- This implementation is tested and known to work against:
-- x Sun's JAVA orbd
--
-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
-- @author "Patrik Karlsson <patrik@cqure.net>"
--

--
-- Version 0.1
-- Created 08/07/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
--

module(... or "giop", package.seeall)

-- A bunch of constants
Constants = {
	
	SyncScope = {
		WITH_TARGET = 3,
	},
	
	ServiceContext = {
		CODESETS = 1,
		SENDING_CONTEXT_RUNTIME = 6,
		NEO_FIRST_SERVICE_CONTEXT = 1313165056,
	},
	
	ReplyStatus = {
		SYSTEM_EXCEPTION = 2,
	},
	
	VERSION_1_0 = 1,
	VERSION_1_2 = 0x0201,
	
	NAMESERVICE = "NameService\0",
}


Packet = {}

Packet.GIOP = {
	
	magic   = "GIOP",
	version = 0x0001,
	byte_order = 0,

	--- Creates a Packet.GIOP
	--
	-- @param msgtype number containing the messaget type
	-- @param data string conatining the message data
	-- @return obj a new Packet.GIOP instance
	new = function( self, msgtype, data )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.type = msgtype
		o.data = data
		o.size = data and #data or 0
		return o
	end,
	
	--- Converts the class to a string suitable to send over the socket
	--
	-- @return string containing the instance data				
	__tostring = function( self )
		return bin.pack("<ASCC>IA", self.magic, self.version, self.byte_order, self.type, self.size, self.data )
	end,
	
	--- Sets the packet version
	--
	-- @param version number containing the version to use
	setVersion = function( self, version ) self.version = version end,
	
	--- Receives the packet over the socket
	--
	-- @param socket containing the already connected socket
	-- @return status true on success, false on failure
	-- @return err containing the error message if status is false
	recv = function( self, socket )
		local status, data = socket:recv( 12 )
		local pos
		
		if ( not(status) ) then	return false, "Failed to read Packet.GIOP" end
		
		pos, self.magic, self.version, self.byte_order, 
		self.type = bin.unpack("<A4SCC", data )
		
		pos, self.size = bin.unpack( ( self.byte_order == 0 and ">" or "<") .. "I", data, pos )
		
		status, data = socket:recv( self.size )
		if ( not(status) ) then	return false, "Failed to read Packet.GIOP" end

		self.data = data
		return true
	end,
}

ServiceContext = {

	--- Creates a ServiceContext
	--
	-- @param id number containing the context id
	-- @param data the service context data
	-- @param pad [optional] number used to pad after the service context
	-- @return obj a new ServiceContext instance
	new = function( self, id, data, pad )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.id = id
		o.data = data or ""
		o.pad = pad
		return o
	end,
	
	--- Converts the class to a string suitable to send over the socket
	--
	-- @return string containing the instance data				
	__tostring = function( self )
		if ( self.pad ) then
			return bin.pack(">IIAS", self.id, #self.data, self.data, self.pad)
		else
			return bin.pack(">IIA", self.id, #self.data, self.data)
		end
	end,
}

--- Creates a SendingContextRuntime
SendingContextRuntime = 
{
	--- Creates a SendingContextRuntime
	--
	-- @param lhost string containing the source ip address
	-- @return obj a new SendingContextRuntime instance
	new = function(self, lhost )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.data = bin.pack(">HIAH", 
			[[ 
				000000000000002849444c3a6f6d672e6f72672f53656e64696e67436f6e746
				578742f436f6465426173653a312e300000000001000000000000006e000102
				00
			]], #lhost + 1, lhost .. "\0", 
			[[
				00ec5100000019afabcb000000000249765d6900000008000000000000000014
				0000000000000200000001000000200000000000010001000000020501000100
				01002000010109000000010001010000000026000000020002
			]] )
		return o
	end,
	
	--- Converts the class to a string suitable to send over the socket
	--
	-- @return string containing the instance data				
	__tostring = function( self ) return self.data end,
}

Packet.GIOP.reply = {
	
	--- Creates a new Packet.GIOP.reply instance
	--
	-- @return obj a new Packet.GIOP.get instance
	new = function( self )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		self.sc = {}
		self.GIOP = Packet.GIOP:new()
		return o
	end,
	
	--- Receives a Packet.GIOP.reply from the socket
	--
	-- @param socket already connected to the server
	-- @return status true on success, false on failure
	-- @return err error message if status is false
	recv = function( self, socket )
		local status, err = self.GIOP:recv( socket )
		local pos, tmp
		local bo = ( self.GIOP.byte_order == 0 and ">" or "<")
		
		if( not(status) ) then return false, err end

		if ( self.GIOP.version == Constants.VERSION_1_2 ) then
			pos, self.request_id, self.reply_status = bin.unpack(bo .. "II", self.GIOP.data, pos )
			pos, tmp = bin.unpack( bo .. "I", self.GIOP.data, pos )
		elseif ( self.GIOP.version == Constants.VERSION_1_0 ) then
			pos, tmp = bin.unpack( bo .. "I", self.GIOP.data )
		end

		for i=1, tmp do
			local ctx_id, ctx_len, ctx_data
			pos, ctx_id, ctx_len = bin.unpack( bo .. "II", self.GIOP.data, pos )
			pos, ctx_data = bin.unpack("A" .. ctx_len, self.GIOP.data, pos )
			if ( i ~= tmp ) then pos = pos + 2 end
			table.insert( self.sc, ServiceContext:new( ctx_id, ctx_data ) )
		end
		
		if ( self.GIOP.version == Constants.VERSION_1_0 ) then
			pos, self.request_id, self.reply_status, self.stub_data = bin.unpack( bo .. "IIA" .. ( #self.GIOP.data - pos - 8 ), self.GIOP.data, pos )
		elseif ( pos < #self.GIOP.data ) then
			pos, self.data = bin.unpack("A" .. (#self.GIOP.data - pos), self.GIOP.data, pos )
		end
				
		return true
	end,
		
}

Packet.GIOP.get = {
	
	resp_expected = 1,
	key_length = 4,
	princ_len = 0,
	
	--- Creates a new Packet.GIOP._is_a instance
	--
	-- @param id the packet identifier
	-- @param key number containing the object key
	-- @param data string containing the stub data
	-- @return obj a new Packet.GIOP.get instance
	new = function( self, id, key, data )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.op = "get\0"
		o.id = id
		o.key = key
		o.data = data
		o.sc = {}
		return o
	end,
	
	--- Creates and adds a service context to the packet
	--
	-- @param id number containing the context id
	-- @param data the service context data
	-- @param pad [optional] number used to pad after the service context
	addServiceContext = function( self, id, data, pad )	table.insert( self.sc, ServiceContext:new(id, data, pad) ) end,
	
	--- Converts the class to a string suitable to send over the socket
	--
	-- @return string containing the packet data				
	__tostring = function( self )
		local data = bin.pack(">I", #self.sc)
		local pad = 0
		
		for i=1, #self.sc do
			local tmp = tostring( self.sc[i])
			data = data .. bin.pack("A", tmp )
		end

		data = data .. bin.pack( ">ICCCCIIIAIA", self.id, self.resp_expected, pad, pad, pad,
					self.key_length, self.key, #self.op, self.op, self.princ_len, self.data )

		return tostring( Packet.GIOP:new( 0, data ) )
	end,
	
}

Packet.GIOP._is_a =
{
	
	--- Creates a new Packet.GIOP._is_a instance
	--
	-- @param id the packet identifier
	-- @param flags [optional]
	-- @param keyaddr string containing the keyaddr data
	-- @return obj a new Packet.GIOP._is_a instance
	new = function( self, id, flags, key_addr )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.op = "_is_a\0"
		o.id = id
		o.target_addr = 0 -- KeyAddr 
		o.key_addr = key_addr
		o.flags = flags or Constants.SyncScope.WITH_TARGET -- SyncScope WITH_TARGET
		o.sc = {}
		return o
	end,
	
	--- Creates and adds a service context to the packet
	--
	-- @param id number containing the context id
	-- @param data the service context data
	-- @param pad [optional] number used to pad after the service context
	addServiceContext = function( self, id, data, pad )	table.insert( self.sc, ServiceContext:new(id, data, pad) )	end,
	
	--- Converts the class to a string suitable to send over the socket
	--
	-- @return string containing the packet data				
	__tostring = function( self )
		local TYPE_ID = "IDL:omg.org/CosNaming/NamingContextExt:1.0\0"
		local RESERVED = 0
		local UNKNOWN, UNKNOWN2, UNKNOWN3 = 2, 1, 0
		local data = bin.pack(">ICCCCSSIAIASI", self.id, self.flags, RESERVED, RESERVED, RESERVED, self.target_addr,
				UNKNOWN, #self.key_addr, self.key_addr, #self.op, self.op, UNKNOWN2, #self.sc )
				
		for i=1, #self.sc do
			local tmp = tostring( self.sc[i])
			data = data .. bin.pack("A", tmp )
		end
		
		data = data .. bin.pack(">IA", #TYPE_ID, TYPE_ID)

		local packet = Packet.GIOP:new( 0, data )
		packet:setVersion( Constants.VERSION_1_2 )
		
		return tostring( packet )	 
	end,
	
}

Packet.GIOP.list = 
{
	--- Creates a new Packet.GIOP.list instance
	--
	-- @param id the packet identifier
	-- @param flags [optional]
	-- @param keyaddr string containing the keyaddr data
	-- @param how_many string containing the value to retrieve
	-- @return obj a new Packet.GIOP.list instance
	new = function( self, id, flags, keyaddr, how_many )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.op = "list\0"
		o.id = id
		o.flags = flags or Constants.SyncScope.WITH_TARGET
		o.target_addr = 0 -- KeyAddr
		o.key_addr = keyaddr
		o.how_many = how_many or 1000
		o.sc = {}
		return o
	end,
	
	--- Creates and adds a service context to the packet
	--
	-- @param id number containing the context id
	-- @param data the service context data
	-- @param pad [optional] number used to pad after the service context
	addServiceContext = function( self, id, data, pad )	table.insert( self.sc, ServiceContext:new(id, data, pad) ) end,

	--- Converts the class to a string suitable to send over the socket
	--
	-- @return string containing the packet data				
	__tostring = function( self )
		local RESERVED = 0
		local UNKNOWN, UNKNOWN2, UNKNOWN3 = 2, 1, 6
		
		local data = bin.pack(">ICCCCSSIAIACCCI", self.id, self.flags, RESERVED, RESERVED,
			RESERVED, self.target_addr, UNKNOWN, #self.key_addr, self.key_addr, 
			#self.op, self.op, RESERVED, RESERVED, UNKNOWN2, #self.sc )
		
		for i=1, #self.sc do
			local tmp = tostring( self.sc[i])
			data = data .. bin.pack("A", tmp )
		end
		
		data = data .. bin.pack(">II", UNKNOWN3, self.how_many )
		local packet = Packet.GIOP:new( 0, data )
		packet:setVersion( Constants.VERSION_1_2 )
		
		return tostring( packet )
	end,
	
}

-- A socket implementation that provides fundamental buffering and allows for
-- reading of an exact number of bytes, instead of atleast ...
Socket =
{	
	new = function(self, socket)
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.Socket = socket or nmap.new_socket()
		o.Buffer = nil
		return o
	end,

	getSrcIp = function( self )
		local status, lhost, _, _, _ = self.Socket:get_info()
		if (not(status)) then return false, "Error failed to get socket information" end
		return true, lhost
	end,
	
	--- Establishes a connection.
	--
	-- @param hostid Hostname or IP address.
	-- @param port Port number.
	-- @param protocol <code>"tcp"</code>, <code>"udp"</code>, or
	-- @return Status (true or false).
	-- @return Error code (if status is false).
	connect = function( self, hostid, port, protocol )
		local status = self.Socket:set_timeout(10000)
		return self.Socket:connect( hostid, port, protocol )
	end,
	
	--- Closes an open connection.
	--
	-- @return Status (true or false).
	-- @return Error code (if status is false).
	close = function( self )
		return self.Socket:close()
	end,
	
	--- Opposed to the <code>socket:receive_bytes</code> function, that returns
	-- at least x bytes, this function returns the amount of bytes requested.
	--
	-- @param count of bytes to read
	-- @return true on success, false on failure
	-- @return data containing bytes read from the socket
	-- 		   err containing error message if status is false
	recv = function( self, count )
		local status, data
	
		self.Buffer = self.Buffer or ""
	
		if ( #self.Buffer < count ) then
			status, data = self.Socket:receive_bytes( count - #self.Buffer )
			if ( not(status) or #data < count - #self.Buffer ) then
				return false, data
			end
			self.Buffer = self.Buffer .. data
		end
			
		data = self.Buffer:sub( 1, count )
		self.Buffer = self.Buffer:sub( count + 1)
	
		return true, data	
	end,
	
	--- Sends data over the socket
	--
	-- @return Status (true or false).
	-- @return Error code (if status is false).
	send = function( self, data )
		return self.Socket:send( data )
	end,
}

-- Static class containing various message decoders
MessageDecoder = {
	
	--- Decodes a get response
	--
	-- @param packet the GIOP packet as recieved by the comm 
	--       <code>exchGIOPPacket</code> function
	-- @return status true on success, false on failure
	-- @return table containing <code>ip</code> and <code>ctx</code>
	["get"] = function( packet )
		local bo = ( packet.GIOP.byte_order == 0 and ">" or "<")
		local pos, len = bin.unpack(bo .. "I", packet.stub_data)
		local ip, ctx
		
		pos = pos + len + 16

		pos, len = bin.unpack(bo .. "I", packet.stub_data, pos)
		pos, ip = bin.unpack( bo .. "A" .. len, packet.stub_data, pos)

		pos = pos + 3
		pos, len = bin.unpack(bo .. "I", packet.stub_data, pos)
		pos, ctx = bin.unpack( bo .. "A" .. len, packet.stub_data, pos)
		
		return true, { ip = ip, ctx = ctx}
	end,
	
	--- Decodes a _is_a response (not implemented)
	--
	-- @param packet the GIOP packet as recieved by the comm 
	--       <code>exchGIOPPacket</code> function
	-- @return status, always true
	["_is_a"] = function( packet )
		return true
	end,

	--- Decodes a list response
	--
	-- @param packet the GIOP packet as recieved by the comm 
	--       <code>exchGIOPPacket</code> function
	-- @return status true on success, false on failure
	-- @return table containing <code>id</code>, <code>kind</code> and
	--         <code>enum</code> or error message if status is false
	["list"] = function( packet )
		local bo = ( packet.GIOP.byte_order == 0 and ">" or "<")
		local pos, seq_len = bin.unpack( bo .. "I", packet.data, 7)
		local objs = {}
		
		for i=1, seq_len do
			local seq_len_of_bind_name
			local len, name
			local obj = {}
			
			pos, seq_len_of_bind_name = bin.unpack( bo .. "I", packet.data, pos)
			if ( seq_len_of_bind_name ~= 1 ) then return false, "Sequence length of Binding_binding_name was greater than 1" end
			
			pos, len = bin.unpack( bo .. "I", packet.data, pos )
			pos, obj.id = bin.unpack( "A" .. len - 1, packet.data, pos )

			-- Account for terminating zero
			pos = pos + 1
			
			-- Account for undecoded data
			pos = pos + ( ( len % 4 > 0 ) and ( 4 - ( len % 4 ) ) or 0 )
			pos = pos + 3
			
			pos, obj.kind = bin.unpack("C", packet.data, pos)
			
			-- Account for undecoded data
			pos = pos + 4
			pos, obj.enum = bin.unpack( bo .. "I", packet.data, pos )
			table.insert( objs, obj )
		end
			
		return true, objs
	end,
	
}

Comm = {
	
	--- Creates a new Comm instance
	--
	-- @param socket containing a buffered socket connected to the server
	-- @return a new Comm instance
	new = function(self, socket)
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.socket = socket
		return o
	end,
	
	--- Sends and recieves a GIOP packet
	--
	-- @param packet containing a Packet.* object, the object must
	--        implement the __tostring meta method
	-- @return status true on success, false on failure
	-- @return data decoder specific data, see the corresponding
	--         MessageDecoder for more information.
	exchGIOPPacket = function( self, packet )
		local status, err = self.socket:send( tostring(packet) )
		local op = packet.op:sub(1, -2)
		local data
		
		if( not(status) ) then return false, err end
		packet = Packet.GIOP.reply:new()

		status, err = packet:recv( self.socket )
		if( not(status) ) then return false, err end

		if ( MessageDecoder[op] ) then
			status, data = MessageDecoder[op]( packet )
		else
			return false, ("No message decoder for op (%s)"):format(op)
		end
		
		return status, data
	end,
	
}


Helper = {
	
	new = function(self, host, port )
		local o = {}
       	setmetatable(o, self)
        self.__index = self
		o.host = host
		o.port = port
		o.socket = Socket:new()
		return o
	end,
	
	GetNamingContext = function( self )
		local packet = Packet.GIOP.get:new( 5, 0x494e4954, bin.pack(">IA", #Constants.NAMESERVICE, Constants.NAMESERVICE) )
		local status, ctx, lhost, pos, len, bo, tmp
				
		packet:addServiceContext( 17, string.char(0x00, 0x02), 0)
		packet:addServiceContext( Constants.ServiceContext.NEO_FIRST_SERVICE_CONTEXT, string.char(0x00, 0x14), 0)
		packet:addServiceContext( Constants.ServiceContext.SENDING_CONTEXT_RUNTIME, tostring(SendingContextRuntime:new( self.lhost )), 0 )
		
		status, packet = self.comm:exchGIOPPacket( packet )
		if( not(status) ) then return status, packet end
				
		return true, packet.ctx
	end,
	
	ListObjects = function( self, keyaddr )
		-- SyncScope WITH_TARGET
		local packet = Packet.GIOP._is_a:new( 5, Constants.SyncScope.WITH_TARGET, keyaddr )
		local status, err, lhost
		
		status, err = self:Reconnect()
		if( not(status) ) then return false, err end
				
		packet:addServiceContext( 17, "\0\2", 0x000d)
		packet:addServiceContext( Constants.ServiceContext.CODESETS, "\0\0\0\0\0\1\0\1\0\1\1\9" )
		packet:addServiceContext( Constants.ServiceContext.NEO_FIRST_SERVICE_CONTEXT, string.char(0x00, 0x14), 0x5d69)
		packet:addServiceContext( Constants.ServiceContext.SENDING_CONTEXT_RUNTIME, tostring(SendingContextRuntime:new( self.lhost )), 0 )
		
		status, packet = self.comm:exchGIOPPacket( packet )
		if( not(status) ) then return status, packet end
		
		packet = Packet.GIOP.list:new( Constants.ServiceContext.SENDING_CONTEXT_RUNTIME, Constants.SyncScope.WITH_TARGET, keyaddr, 1000 )
		packet:addServiceContext( 17, "\0\2", 0x000d)
		packet:addServiceContext( Constants.ServiceContext.CODESETS, "\0\0\0\0\0\1\0\1\0\1\1\9" )
		packet:addServiceContext( Constants.ServiceContext.NEO_FIRST_SERVICE_CONTEXT, string.char(0x00, 0x14), 0x9c9b)
		
		status, packet = self.comm:exchGIOPPacket( packet )
		if( not(status) ) then return status, packet end
		
		return true, packet
	end,
	
	--- Connects and performs protocol negotiation with the Oracle server
	--
	-- @return true on success, false on failure
	-- @return err containing error message when status is false
	Connect = function( self )
		local status, data = self.socket:connect( self.host.ip, self.port.number, "tcp" )
		if( not(status) ) then return status, data end
		self.comm = Comm:new( self.socket )

		status, self.lhost = self.socket:getSrcIp()
		if ( not(status) ) then 
			self.socket:close()
			return false, self.lhost 
		end

		return true
	end,
	
	Close = function( self )
		return self.socket:close()
	end,
	
	Reconnect = function( self )
		local status = self:Close()
		if( not(status) ) then return false, "Failed to close socket" end
		
		status = self:Connect()
		if( not(status) ) then return false, "Failed to re-connect socket" end

		return true
	end,
}