/nselib/rmi.lua
Lua | 1548 lines | 942 code | 167 blank | 439 comment | 147 complexity | d4bd8afdf7c067a31fb964813d0c9461 MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-2.0, Apache-2.0, LGPL-2.0, LGPL-2.1, MIT
- --- Library method for communicating over RMI (JRMP + java serialization)
- --
- -- This is a not complete RMI implementation for Lua, which is meant to be able
- -- to invoke methods and parse returnvalues which are simple, basically the java primitives.
- -- This can be used to e.g dump out the registry, and perform authentication against
- -- e.g JMX-services.
- --
- -- This library also contains some classes which works pretty much like the
- -- java classes BufferedReader, BufferedWriter, DataOutputStream and DataInputStream.
- --
- -- Most of the methods in the RMIDataStream class is based on the OpenJDK RMI Implementation,
- -- and I have kept the methodnames as they are in java, so it should not be too hard to find
- -- the corresponding functionality in the jdk codebase to see how things 'should' be done, in case
- -- there are bugs or someone wants to make additions. I have only implemented the
- -- things that were needed to get things working, but it should be pretty simple to add more
- -- functionality by lifting over more stuff from the jdk.
- --
- -- The interesting classes in OpenJDK are:
- -- java.io.ObjectStreamConstants
- -- java.io.ObjectStreamClass
- -- java.io.ObjectInputStream
- -- sun.rmi.transport.StreamRemoteCall
- -- and a few more.
- --
- -- If you want to add calls to classes you know of, you can use e.g Jode to decompile the
- -- stub-class or skeleton class and find out the details that are needed to perform an
- -- RMI method invocation. Those are
- -- Class hashcode
- -- Method number (each method gets a number)
- -- Arguments f
- -- You also need the object id (so the remote server knows what instance you are talking to). That can be
- -- fetched from the registry (afaik) but not currently implemented. Some object ids are static : the registry is always 0
- --
- -- @author Martin Holst Swende
- -- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
- -- @see java 1.4 RMI-spec: http://java.sun.com/j2se/1.4.2/docs/guide/rmi/
- -- @see java 5 RMI-spec: http://java.sun.com/j2se/1.5.0/docs/guide/rmi/spec/rmiTOC.html
- -- @see java 6 RMI-spec : http://java.sun.com/javase/6/docs/technotes/guides/rmi/index.html
- -- @see The protocol for Java object serializtion : http://java.sun.com/javase/6/docs/platform/serialization/spec/protocol.html
- -- Version 0.2
- -- Created 09/06/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
- -- Fixed more documentation - v0.2 Martin Holst Swende <martin@swende.se>
- local bin = require "bin"
- local bit = require "bit"
- local nmap = require "nmap"
- local stdnse = require "stdnse"
- local string = require "string"
- local table = require "table"
- _ENV = stdnse.module("rmi", stdnse.seeall)
- -- Some lazy shortcuts
- local function dbg(str,...)
- local arg={...}
- stdnse.debug3("RMI:"..str, table.unpack(arg))
- end
- -- Convenience function to both print an error message and return <false, msg>
- -- Example usage :
- -- if foo ~= "gazonk" then
- -- return doh("Foo should be gazonk but was %s", foo)
- -- end
- local function doh(str,...)
- local arg={...}
- stdnse.debug1("RMI-ERR:"..tostring(str), table.unpack(arg))
- return false, str
- end
- ---
- -- BufferedWriter
- -- This buffering writer provide functionality much like javas BufferedWriter.
- --
- -- BufferedWriter wraps the pack-functionality from bin, and buffers data internally
- -- until flush is called. When flush is called, it either sends the data to the socket OR
- -- returns the data, if no socket has been set.
- --@usage:
- -- local bWriter = BufferedWriter:new(socket)
- -- local breader= BufferedReader:new(socket)
- --
- -- bWriter.pack('>i', integer)
- -- bWriter.flush() -- sends the data
- --
- -- if bsocket:canRead(4) then -- Waits until four bytes can be read
- -- local packetLength = bsocket:unpack('i') -- Read the four bytess
- -- if bsocket:canRead(packetLength) then
- -- -- ...continue reading packet values
- BufferedWriter = {
- new = function(self, socket)
- local o = {}
- setmetatable(o, self)
- self.__index = self
- o.writeBuffer = ''
- o.pos = 1
- o.socket = socket
- return o
- end,
- -- Sends data over the socket
- -- (Actually, just buffers until flushed)
- -- @return Status (true or false).
- -- @return Error code (if status is false).
- send = function( self, data )
- self.writeBuffer = self.writeBuffer .. data
- end,
- -- Convenience function, wraps bin
- pack = function(self, fmt, ... )
- local arg={...}
- self.writeBuffer = self.writeBuffer .. bin.pack( fmt, table.unpack(arg))
- end,
- -- This function flushes the buffer contents, thereby emptying
- -- the buffer. If a socket has been supplied, that's where it will be sent
- -- otherwise the buffer contents are returned
- --@return status
- --@return content of buffer, in case no socket was used
- flush = function(self)
- local content = self.writeBuffer
- self.writeBuffer = ''
- if not self.socket then
- return true, content
- end
- return self.socket:send(content)
- end,
- }
- ---
- -- BufferedReader reads data from the supplied socket and contains functionality
- -- to read all that is available and store all that is not currently needed, so the caller
- -- gets an exact number of bytes (which is not the case with the basic nmap socket implementation)
- -- If not enough data is available, it blocks until data is received, thereby handling the case
- -- if data is spread over several tcp packets (which is a pitfall for many scripts)
- --
- -- It wraps unpack from bin for the reading.
- -- OBS! You need to check before invoking skip or unpack that there is enough
- -- data to read. Since this class does not parse arguments to unpack, it does not
- -- know how much data to read ahead on those calls.
- --@usage:
- -- local bWriter = BufferedWriter:new(socket)
- -- local breader= BufferedReader:new(socket)
- --
- -- bWriter.pack('>i', integer)
- -- bWriter.flush() -- sends the data
- --
- -- if bsocket:canRead(4) then -- Waits until four bytes can be read
- -- local packetLength = bsocket:unpack('i') -- Read the four bytess
- -- if bsocket:canRead(packetLength) then
- -- -- ...continue reading packet values
- BufferedReader = {
- new = function(self, socket, readBuffer)
- local o = {}
- setmetatable(o, self)
- self.__index = self
- o.readBuffer = readBuffer -- May be nil
- o.pos = 1
- o.socket = socket -- May also be nil
- return o
- end,
- ---
- -- This method blocks until the specified number of bytes
- -- have been read from the socket and are available for
- -- the caller to read, e.g via the unpack function
- canRead= function(self,count)
- local status, data
- self.readBuffer = self.readBuffer or ""
- local missing = self.pos + count - #self.readBuffer -1
- if ( missing > 0) then
- if self.socket == nil then
- return doh("Not enough data in static buffer")
- end
- status, data = self.socket:receive_bytes( missing )
- if ( not(status) ) then
- return false, data
- end
- self.readBuffer = self.readBuffer .. data
- end
- -- Now and then, we flush the buffer
- if ( self.pos > 1024) then
- self.readBuffer = self.readBuffer:sub( self.pos )
- self.pos = 1
- end
- return true
- end,
- ---
- --@return Returns the number of bytes already available for reading
- bufferSize = function(self)
- return #self.readBuffer +1 -self.pos
- end,
- ---
- -- This function works just like bin.unpack (in fact, it is
- -- merely a wrapper around it. However, it uses the data
- -- already read into the buffer, and the internal position
- --@param format - see bin
- --@return the unpacked value (NOT the index)
- unpack = function(self,format)
- local ret = {bin.unpack(format, self.readBuffer, self.pos)}
- self.pos = ret[1]
- return table.unpack(ret,2)
- end,
- ---
- -- This function works just like bin.unpack (in fact, it is
- -- merely a wrapper around it. However, it uses the data
- -- already read into the buffer, and the internal position.
- -- This method does not update the current position, and the
- -- data can be read again
- --@param format - see bin
- --@return the unpacked value (NOT the index)
- peekUnpack = function(self,format)
- local ret = {bin.unpack(format, self.readBuffer, self.pos)}
- return table.unpack(ret,2)
- end,
- ---
- -- Tries to read a byte, without consuming it.
- --@return status
- --@return bytevalue
- peekByte = function(self)
- if self:canRead(1) then
- return true, self:peekUnpack('C')
- end
- return false
- end,
- ---
- -- Skips a number of bytes
- --@param len the number of bytes to skip
- skip = function(self, len)
- if(#self.readBuffer < len + self.pos) then
- return doh("ERROR: reading too far ahead")
- end
- local skipped = self.readBuffer:sub(self.pos, self.pos+len-1)
- self.pos = self.pos + len
- return true, skipped
- end,
- }
- -- The classes are generated when this file is loaded, by the definitions in the JavaTypes
- -- table. That table contains mappings between the format used by bin and the types
- -- available in java, aswell as the lengths (used for availability-checks) and the name which
- -- is prefixed by read* or write* when monkey-patching the classes and adding functions.
- -- For example: {name = 'Int', expr = '>i', len= 4}, will generate the functions
- -- writeInt(self, value) and readInt() respectively
- local JavaTypes = {
- {name = 'Int', expr = '>i', len= 4},
- {name = 'UnsignedInt', expr = '>I', len= 4},
- {name = 'Short', expr = '>s', len= 2},
- {name = 'UnsignedShort', expr = '>S', len= 2},
- {name = 'Long', expr = '>l', len= 8},
- {name = 'UnsignedLong', expr = '>L', len= 8},
- {name = 'Byte', expr = '>C', len= 1},
- }
- ---
- -- The JavaDOS classes
- -- The JavaDOS class is an approximation of a java DataOutputStream. It provides convenience functions
- -- for writing java types to an underlying BufferedWriter
- --
- -- When used in conjunction with the BufferedX- classes, they handle the availability-
- -- checks transparently, i.e the caller does not have to check if enough data is available
- --
- -- @usage:
- -- local dos = JavaDOS:new(BufferedWriter:new(socket))
- -- local dos = JavaDIS:new(BufferedReader:new(socket))
- -- dos:writeUTF("Hello world")
- -- dos:writeInt(3)
- -- dos:writeLong(3)
- -- dos:flush() -- send data
- -- local answer = dis:readUTF()
- -- local int = dis:readInt()
- -- local long = dis:readLong()
- JavaDOS = {
- new = function (self,bWriter)
- local o = {} -- create new object if user does not provide one
- setmetatable(o, self)
- self.__index = self -- DIY inheritance
- o.bWriter = bWriter
- return o
- end,
- -- This closure method generates all writer methods on the fly
- -- according to the definitions in JavaTypes
- _generateWriterFunc = function(self, javatype)
- local functionName = 'write'..javatype.name
- local newFunc = function(_self, value)
- --dbg(functionName .."(%s) called" ,tostring(value))
- return _self:pack(javatype.expr, value)
- end
- self[functionName] = newFunc
- end,
- writeUTF = function(self, text)
- -- TODO: Make utf-8 of it
- return self:pack('>P', text)
- end,
- pack = function(self, ...)
- local arg={...}
- return self.bWriter:pack(table.unpack(arg))
- end,
- write = function(self, data)
- return self.bWriter:send(data)
- end,
- flush = function(self)
- return self.bWriter:flush()
- end,
- }
- ---
- -- The JavaDIS class
- -- JavaDIS is close to java DataInputStream. It provides convenience functions
- -- for reading java types from an underlying BufferedReader
- --
- -- When used in conjunction with the BufferedX- classes, they handle the availability-
- -- checks transparently, i.e the caller does not have to check if enough data is available
- --
- -- @usage:
- -- local dos = JavaDOS:new(BufferedWriter:new(socket))
- -- local dos = JavaDIS:new(BufferedReader:new(socket))
- -- dos:writeUTF("Hello world")
- -- dos:writeInt(3)
- -- dos:writeLong(3)
- -- dos:flush() -- send data
- -- local answer = dis:readUTF()
- -- local int = dis:readInt()
- -- local long = dis:readLong()
- JavaDIS = {
- new = function (self,bReader)
- local o = {} -- create new object if user does not provide one
- setmetatable(o, self)
- self.__index = self -- DIY inheritance
- o.bReader = bReader
- return o
- end,
- -- This closure method generates all reader methods (except nonstandard ones) on the fly
- -- according to the definitions in JavaTypes.
- _generateReaderFunc = function(self, javatype)
- local functionName = 'read'..javatype.name
- local newFunc = function(_self)
- --dbg(functionName .."() called" )
- if not _self.bReader:canRead(javatype.len) then
- local err = ("Not enough data in buffer (%d required by %s)"):format(javatype.len, functionName)
- return doh(err)
- end
- return true, _self.bReader:unpack(javatype.expr)
- end
- self[functionName] = newFunc
- end,
- -- This is a bit special, since we do not know beforehand how many bytes must be read. Therefore
- -- this cannot be generated on the fly like the others.
- readUTF = function(self, text)
- -- First, we need to read the length, 2 bytes
- if not self.bReader:canRead(2) then-- Length of the string is two bytes
- return false, "Not enough data in buffer [0]"
- end
- -- We do it as a 'peek', so bin can reuse the data to unpack with 'P'
- local len = self.bReader:peekUnpack('>S')
- --dbg("Reading utf, len %d" , len)
- -- Check that we have data
- if not self.bReader:canRead(len) then
- return false, "Not enough data in buffer [1]"
- end
- -- For some reason, the 'P' switch does not work for me.
- -- Probably some idiot thing. This is a hack:
- local val = self.bReader.readBuffer:sub(self.bReader.pos+2, self.bReader.pos+len+2-1)
- self.bReader.pos = self.bReader.pos+len+2
- -- Someone smarter than me can maybe get this working instead:
- --local val = self.bReader:unpack('P')
- --dbg("Read UTF: %s", val)
- return true, val
- end,
- readLongAsHexString = function(self)
- if not self.bReader:canRead(8) then-- Length of the string is two bytes
- return false, "Not enough data in buffer [3]"
- end
- return true, self.bReader:unpack('H8')
- end,
- skip = function(self, len)
- return self.bReader:skip(len)
- end,
- canRead = function(self, len)
- return self.bReader:canRead(len)
- end,
- }
- -- Generate writer-functions on the JavaDOS/JavaDIS classes on the fly
- for _,x in ipairs(JavaTypes) do
- JavaDOS._generateWriterFunc(JavaDOS, x)
- JavaDIS._generateReaderFunc(JavaDIS, x)
- end
- ---
- -- This class represents a java class and is what is returned by the library
- -- when invoking a remote function. Therefore, this can also represent a java
- -- object instance.
- JavaClass = {
- new = function(self)
- local o = {}
- setmetatable(o, self)
- self.__index = self
- return o
- end,
- customDataFormatter = nil,
- setName = function( self, name )
- dbg("Setting class name to %s", name)
- self.name = name
- end,
- setSerialID = function( self, serial ) self.serial = serial end,
- setFlags = function( self, flags )
- self.flags = RMIUtils.flagsToString(flags)
- self._binaryflags = flags
- end,
- isExternalizable = function(self)
- if self._binaryFlags == nil then return false end
- return bit.band(self._binaryflags, RMIUtils.SC_EXTERNALIZABLE)
- end,
- addField = function( self, field )
- if self.fields == nil then self.fields = {} end
- table.insert( self.fields, field )
- --self[field.name] = field
- end,
- setSuperClass = function(self,super) self.superClass = super end,
- setCustomData = function(self, data) self.customData = data end,
- getCustomData = function(self) return self.customData end,
- setInterfaces = function(self,ifaces) self.ifaces = ifaces end,
- getName = function( self ) return self.name end,
- getSuperClass = function(self) return self.superClass end,
- getSerialID = function( self ) return self.serial end,
- getFlags = function( self ) return self.flags end,
- getFields = function( self ) return self.fields end,
- getFieldByName = function( self, name )
- if self.fields == nil then return end
- for i=1, #self.fields do
- if ( self.fields[i].name == name ) then
- return self.fields[i]
- end
- end
- end,
- __tostring = function( self )
- local data = {}
- if self.name ~=nil then
- data[#data+1] = ("%s "):format(self.name)
- else
- data[#data+1] = "???"
- end
- if self.superClass~=nil then
- data[#data+1] = " extends ".. tostring( self.superClass)
- end
- if self.ifaces ~= nil then
- data[#data+1] = " implements " .. self.ifaces
- end
- if self.fields ~=nil then
- for i=1, #self.fields do
- if i == 1 then
- data[#data+1] = "["
- end
- data[#data+1] = tostring(self.fields[i])
- if ( i < #self.fields ) then
- data[#data+1] = ";"
- else
- data[#data+1] = "]"
- end
- end
- end
- return table.concat(data)
- end,
- toTable = function(self, customDataFormatter)
- local data = {self.name}
- if self.externalData ~=nil then
- table.insert(data, tostring(self.externalData))
- end
- --if self.name ~=nil then
- -- data.class = self.name
- --end
- if self.ifaces ~= nil then
- table.insert(data, " implements " .. self.ifaces)
- end
- if self.superClass~=nil then
- local extends = self.superClass:toTable()
- table.insert(data ,"extends")
- table.insert(data, extends)
- --data.extends = self.superClass:toTable()
- end
- if self.fields ~=nil then
- table.insert(data, "fields")
- local f = {}
- for i=1, #self.fields do
- table.insert(f, self.fields[i]:toTable())
- end
- table.insert(data, f)
- end
- if self.customData ~=nil then
- local formatter = JavaClass['customDataFormatter']
- if formatter ~= nil then
- local title, cdata = formatter(self.name, self.customData)
- table.insert(data, title)
- table.insert(data, cdata)
- else
- table.insert(data, "Custom data")
- table.insert(data, self.customData)
- end
- end
- return data
- end,
- }
- --- Represents a field in an object, i.e an object member
- JavaField = {
- new = function(self, name, typ )
- local o = {}
- setmetatable(o, self)
- self.__index = self
- o.name = name
- o.type = typ
- return o
- end,
- setType = function( self, typ ) self.type = typ end,
- setSignature = function( self, sig ) self.signature = sig end,
- setName = function( self, name ) self.name = name end,
- setObjectType = function( self, ot ) self.object_type = ot end,
- setReference = function( self, ref ) self.ref = ref end,
- setValue = function (self, val)
- dbg("Setting field value to %s", tostring(val))
- self.value = val
- end,
- getType = function( self ) return self.type end,
- getSignature = function( self ) return self.signature end,
- getName = function( self ) return self.name end,
- getObjectType = function( self ) return self.object_type end,
- getReference = function( self ) return self.ref end,
- getValue = function( self ) return self.value end,
- __tostring = function( self )
- if self.value ~= nil then
- return string.format("%s %s = %s", self.type, self.name, self.value)
- else
- return string.format("%s %s", self.type, self.name)
- end
- end,
- toTable = function(self)
- local data = {tostring(self.type) .. " " .. tostring(self.name)}
- --print("FIELD VALUE:", self.value)
- if self.value ~= nil then
- if type(self.value) == 'table' then
- if self.value.toTable ~=nil then
- table.insert(data, self.value:toTable())
- else
- table.insert(data, self.value)
- end
- else
- table.insert(data, self.value)
- end
- end
- return data
- end,
- }
- ---
- -- Represents a java array. Internally, this is a lua list of JavaClass-instances
- JavaArray = {
- new = function(self)
- local o = {}
- setmetatable(o, self)
- self.__index = self
- o.values = {}
- return o
- end,
- setClass = function( self, class ) self.class = class end,
- setLength = function( self, length ) self.length = length end,
- setValue = function(self, index, object) self.values[index] = object end,
- __tostring=function(self)
- local data = {
- ("Array: %s [%d] = {"):format(tostring(self.class), self.length)
- }
- for i=1, #self.values do
- data[#data+1] = self.values[i]..","
- end
- data[#data+1] = "}"
- return table.concat(data)
- end,
- toTable = function(self)
- local title = ("Array: %s [%d] = {"):format(tostring(self.class), self.length)
- local t = {title = self.values}
- return t
- end,
- getValues = function(self) return self.values end
- }
- TC = {
- TC_NULL = 0x70,
- TC_REFERENCE = 0x71,
- TC_CLASSDESC = 0x72,
- TC_OBJECT = 0x73,
- TC_STRING = 0x74,
- TC_ARRAY = 0x75,
- TC_CLASS = 0x76,
- TC_BLOCKDATA = 0x77,
- TC_ENDBLOCKDATA = 0x78,
- TC_RESET = 0x79,
- TC_BLOCKDATALONG = 0x7A,
- TC_EXCEPTION = 0x7B,
- TC_LONGSTRING = 0x7C,
- TC_PROXYCLASSDESC = 0x7D,
- TC_ENUM = 0x7E,
- Integer = 0x49,
- Object = 0x4c,
- Strings = {
- [0x49] = "Integer",
- [0x4c] = "Object",
- [0x71] = "TC_REFERENCE",
- [0x70] = "TC_NULL",
- [0x71] = "TC_REFERENCE",
- [0x72] = "TC_CLASSDESC",
- [0x73] = "TC_OBJECT",
- [0x74] = "TC_STRING",
- [0x75] = "TC_ARRAY",
- [0x76] = "TC_CLASS",
- [0x77] = "TC_BLOCKDATA",
- [0x78] = "TC_ENDBLOCKDATA",
- [0x79] = "TC_RESET",
- [0x7A] = "TC_BLOCKDATALONG",
- [0x7B] = "TC_EXCEPTION",
- [0x7C] = "TC_LONGSTRING",
- [0x7D] = "TC_PROXYCLASSDESC",
- [0x7E] = "TC_ENUM",
- },
- }
- local Version= 0x02
- local Proto= {Stream=0x4b, SingleOp=0x4c, Multiplex=0x4d}
- ---
- -- RmiDataStream class
- -- This class can handle reading and writing JRMP, i.e RMI wire protocol and
- -- can do some very limited java deserialization. This implementation has
- -- borrowed from OpenJDK RMI implementation, but only implements an
- -- absolute minimum of what is required in order to perform some basic calls
- --
- RmiDataStream = {
- new = function (self,o)
- o = o or {} -- create object if user does not provide one
- setmetatable(o, self)
- self.__index = self -- DIY inheritance
- return o
- end,
- }
- -- An output stream in RMI consists of transport Header information followed by a sequence of Messages.
- -- Out:
- -- Header Messages
- -- HttpMessage
- -- Header:
- -- 0x4a 0x52 0x4d 0x49 Version Protocol
- -- (4a 52 4d 49 === JRMI)
- -- Version:
- -- 0x00 0x01
- -- Protocol:
- -- StreamProtocol
- -- SingleOpProtocol
- -- MultiplexProtocol
- -- StreamProtocol:
- -- 0x4b
- -- SingleOpProtocol:
- -- 0x4c
- -- MultiplexProtocol:
- -- 0x4d
- -- Messages:
- -- Message
- -- Messages Message
- ----
- -- Connects to a remote service. The connection process creates a
- -- socket and does some handshaking. If this is successful,
- -- we are definitely talking to an RMI service.
- function RmiDataStream:connect(host, port)
- local status, err
- local socket = nmap.new_socket()
- socket:set_timeout(5000)
- -- local bsocket = BufferedSocket:new()
- socket:connect(host,port, "tcp")
- -- Output and input
- local dos = JavaDOS:new(BufferedWriter:new(socket))
- local dis = JavaDIS:new(BufferedReader:new(socket))
- -- Start sending a message --
- -- Add Header, Version and Protocol
- --dos:write('JRMI' .. bin.pack('H', Version .. Proto.Stream))
- dos:writeInt(1246907721) -- == JRMI
- dos:writeShort(Version)
- dos:writeByte(Proto.Stream)
- status = dos:flush()
- if not status then
- return doh(err)
- end
- -- For the StreamProtocol and the MultiplexProtocol, the server must respond with a a byte 0x4e
- -- acknowledging support for the protocol, and an EndpointIdentifier that contains the host name
- -- and port number that the server can see is being used by the client.
- -- The client can use this information to determine its host name if it is otherwise unable to do that for security reasons.
- -- Read ack
- status, err = self:readAck(dis)
- if not status then
- return doh("No ack received from server:" .. tostring(err))
- end
- -- The client must then respond with another EndpointIdentifier that contains the clients
- -- default endpoint for accepting connections. This can be used by a server in the MultiplexProtocol case to identify the client.
- dos:writeUTF("127.0.0.1") -- TODO, write our own ip instead (perhaps not necessary, since we are not using MultiplexProtocol
- dos:writeInt(0) -- Port ( 0 works fine)
- dos:flush()
- self.dos = dos
- self.dis =dis
- return true
- end
- -- Reads a DgcAck message, which is sent during connection handshake
- --@param dis - a JavaDIS to read from
- --@return status
- --@return error message
- function RmiDataStream:readAck(dis)
- local status, ack = dis:readByte()
- if not status then return doh( "Could not read data") end
- if ack ~= 78 then
- return doh("No ack received: ".. tostring(ack))
- end
- local status, host = dis:readUTF()
- if not status then return false, "Could not read data" end
- local status, port = dis:readUnsignedInt()
- if not status then return false, "Could not read data" end
- dbg("RMI-Ack received (host %s, port: %d) " , host, port)
- return true
- end
- -- Sends an RMI method call
- --@param out - a JavaDos outputstream
- --@param objNum -object id (target of call)
- --@param hash - the hashcode for the class that is invoked
- --@param op - the operation number (method) invoked
- --@param arguments - optional, if arguments are needed to this method. Should be an Arguments table
- -- or something else which has a getData() function to get binary data
- function RmiDataStream:writeMethodCall(out,objNum, hash, op, arguments)
- dbg("Invoking object %s, hash %s, opNum %s, args %s", tostring(objNum), tostring(hash), tostring(op), tostring(arguments))
- local dos = self.dos
- local dis = self.dis
- -- Send Call:
- dos:writeByte(0x50)
- -- Send Magic 0xaced
- dos:writeShort(0xACED)
- -- Send version 0x0005
- dos:writeShort(0x0005)
- -- Send TC_BLOKDATA
- dos:writeByte(0x77)
- -- send length (byte)
- dos:writeByte(0x22)
- -- From sun.rmi.transport.StreamRemoteCall :
- -- // write out remote call header info...
- -- // call header, part 1 (read by Transport)
- -- conn.getOutputStream().write(TransportConstants.Call);
- -- getOutputStream(); // creates a MarshalOutputStream
- -- id.write(out); // object id (target of call)
- -- // call header, part 2 (read by Dispatcher)
- -- out.writeInt(op); // method number (operation index)
- -- out.writeLong(hash); // stub/skeleton hash
- -- Send rest of the call
- local unique, time, count =0,0,0
- dos:writeLong(objNum);-- id objNum
- dos:writeInt(unique); -- space
- dos:writeLong(time);
- dos:writeShort(count);
- dos:writeInt(op)
- dos:pack('H',hash)
- -- And now, the arguments
- if arguments ~= nil then
- dos:write(arguments:getData())
- end
- dos:flush()
- end
- ---
- -- Invokes a method over RMI
- --@param methodData, a table which should contain the following
- --@param objNum -object id (target of call)
- --@param hash - the hashcode for the class that is invoked
- --@param op - the operation number (method) invoked
- --@param arguments - optional, if arguments are needed to this method. Should be an Arguments table
- -- or something else which has a getData() function to get binary data
- --@return status
- --@return a JavaClass instance
- function RmiDataStream:invoke(objNum, hash, op, arguments)
- local status, data
- local out = self.out
- local dis = self.dis
- self:writeMethodCall(out,objNum,hash, op, arguments)
- local status, retByte = dis:readByte()
- if not status then return false, "No return data received from server" end
- if 0x51 ~= retByte then -- 0x51 : Returndata
- return false, "No return data received from server"
- end
- status, data = self:readReturnData(dis)
- return status, data
- end
- ---
- -- Reads an RMI ReturnData packet
- --@param dis a JavaDIS inputstream
- function RmiDataStream:readReturnData(dis)
- --[[
- From -http://turtle.ee.ncku.edu.tw/docs/java/jdk1.2.2/guide/rmi/spec/rmi-protocol.doc3.html :
- A ReturnValue of an RMI call consists of a return code to indicate either a normal or
- exceptional return, a UniqueIdentifier to tag the return value (used to send a DGCAck if necessary)
- followed by the return result: either the Value returned or the Exception thrown.
- CallData: ObjectIdentifier Operation Hash (Arguments)
- ReturnValue:
- 0x01 UniqueIdentifier (Value)
- 0x02 UniqueIdentifier Exception
- ObjectIdentifier: ObjectNumber UniqueIdentifier
- UniqueIdentifier: Number Time Count
- Arguments: Value Arguments Value
- Value: Object Primitive
- Example: [ac ed][00 05][77][0f][01][25 14 95 21][00 00 01 2b 16 9a 62 5a 80 0b]
- [magc][ver ][BL][L ][Ok][ --------------- not interesting atm ----------------------]
- --]]
- -- We need to be able to read at least 7 bytes
- -- If that is doable, we can ignore the status on the following readbyte operations
- if not dis:canRead(7) then
- return doh("Not enough data received")
- end
- local status, magic = dis:readShort() -- read magic
- local status, version = dis:readShort() -- read version
- local status, typ = dis:readByte()
- if typ ~= TC.TC_BLOCKDATA then
- return doh("Expected block data when reading return data")
- end
- local status, len = dis:readByte() -- packet length
- --dis:setReadLimit(len)
- local status, ex = dis:readByte() -- 1=ok, 2=exception thrown
- if ex ~= 1 then
- return doh("Remote call threw exception")
- end
- -- We can skip the rest of this block
- dis:skip(len -1)
- -- Now, the return value object:
- local status, x = readObject0(dis)
- dbg("Read object, got %d left in buffer", dis.bReader:bufferSize())
- if(dis.bReader:bufferSize() > 0) then
- local content = dis.bReader:unpack('H'..tostring(dis.bReader:bufferSize()))
- dbg("Buffer content: %s" ,content)
- end
- return status, x
- end
- ---
- -- Deserializes a serialized java object
- function readObject0(dis)
- local finished = false
- local data, status, responseType
- status, responseType = dis:readByte()
- if not status then
- return doh("Not enough data received")
- end
- dbg("Reading object of type : %s" , RMIUtils.tcString(responseType))
- local decoder = TypeDecoders[responseType]
- if decoder ~= nil then
- status, data = decoder(dis)
- if not status then return doh("readObject0: Could not read data %s", tostring(data)) end
- dbg("Read: %s", tostring(data))
- return true, data
- else
- return doh("No decoder found for responsetype: %s" , RMIUtils.tcString(responseType))
- end
- end
- function readString(dis)
- return dis:readUTF()
- end
- -- Reads return type array
- function readArray(dis)
- local array = JavaArray:new()
- dbg("Reading array class description")
- local status, classDesc = readClassDesc(dis)
- array:setClass(classDesc)
- dbg("Reading array length")
- local status, len = dis:readInt()
- if not status then
- return doh("Could not read data")
- end
- array:setLength(len)
- dbg("Reading array of length is %X", len)
- for i =1, len, 1 do
- local status, object = readObject0(dis)
- array:setValue(i,object)
- end
- return true, array
- end
- function readClassDesc(dis)
- local status, p = dis:readByte()
- if not status then return doh( "Could not read data" ) end
- dbg("reading classdesc: %s" , RMIUtils.tcString(p))
- local val
- if p == TC.TC_CLASSDESC then
- dbg("Reading TC_CLASSDESC")
- status, val = readNonProxyDesc(dis)
- elseif p == TC.TC_NULL then
- dbg("Reading TC_NULL")
- status, val = true, nil
- elseif p == TC.TC_PROXYCLASSDESC then
- dbg("Reading TC_PROXYCLASSDESC")
- status, val = readProxyDesc(dis)
- else
- return doh("TC_classdesc is other %d", p)
- end
- if not status then
- return doh("Error reading class description")
- end
- return status, val
- end
- function readOrdinaryObject(dis)
- local status, desc = readClassDesc(dis)
- if not status then
- return doh("Error reading ordinary object")
- end
- if desc:isExternalizable() then
- dbg("External content")
- local status, extdata = readExternalData(dis)
- if status then
- desc["externalData"] = extdata
- end
- else
- dbg("Serial content")
- local status, serdata = readExternalData(dis)
- if status then
- desc["externalData"] = serdata
- local status, data =parseExternalData(desc)
- if status then
- desc['externalData'] = data
- end
- end
- end
- return status, desc
- end
- -- Attempts to read some object-data, at least remove the block
- -- header. This method returns the external data in 'raw' form,
- -- since it is up to each class to define an readExternal method
- function readExternalData(dis)
- local data = {}
- while dis.bReader:bufferSize() > 0 do
- local status, tc= dis:readByte()
- if not status then
- return doh("Could not read external data")
- end
- dbg("readExternalData: %s", RMIUtils.tcString(tc))
- local status, len, content
- if tc == TC.TC_BLOCKDATA then
- status, len = dis:readByte()
- status, content = dis.bReader:skip(len)
- --print(bin.unpack("H"..tostring(#content),content))
- --print(makeStringReadable(content))
- dbg("Read external data (%d bytes): %s " ,len, content)
- --local object = ExternalClassParsers['java.rmi.server.RemoteObject'](dis)
- --print(object)
- return status, content
- elseif tc == TC.TC_BLOCKDATALONG then
- status, len = dis:readUnsignedInt()
- status, content = dis.bReader:skip(len)
- return status, content
- elseif tc == TC.TC_ENDBLOCKDATA then
- --noop
- else
- return doh("Got unexpected field in readExternalData: %s ", RMIUtils.tcString(tc))
- end
- end
- end
- ----
- -- ExternalClassParsers : External Java Classes
- -- This 'class' contains information about certain specific java classes,
- -- such as UnicastRef, UnicastRef2. After such an object has been read by
- -- the object serialization protocol, it will contain a lump of data which is
- -- in 'external' form, and needs to be read in a way which is specific for the class
- -- itself. This class contains the implementations for reading out the
- -- 'goodies' of e.g UnicastRef, which contain important information about
- -- where another RMI-socket is listening and waiting for someone to connect.
- ExternalClassParsers = {
- ---
- --@see sun.rmi.transport.tcp.TCPEndpoint
- --@see sun.rmi.server.UnicastRef
- --@see sun.rmi.server.UnicastRef2
- UnicastRef = function(dis)
- local stat, host = dis:readUTF();
- if not stat then return doh("Parsing external data, could not read host (UTF)") end
- local status, port = dis:readUnsignedInt();
- if not stat then return doh("Parsing external data, could not read port (int)") end
- dbg("a host: %s, port %d", host, port)
- return true, ("@%s:%d"):format(host,port)
- end,
- ---
- --@see sun.rmi.transport.tcp.TCPEndpoint
- --@see sun.rmi.server.UnicastRef
- --@see sun.rmi.server.UnicastRef2
- UnicastRef2 = function(dis)
- local stat, form = dis:readByte();
- if not stat then return doh("Parsing external data, could not read byte") end
- if form == 0 or form == 1 then-- FORMAT_HOST_PORT or FORMAT_HOST_PORT_FACTORY
- local stat, host = dis:readUTF();
- if not stat then return doh("Parsing external data, could not read host (UTF)") end
- local status, port = dis:readUnsignedInt();
- if not stat then return doh("Parsing external data, could not read port (int)") end
- dbg("b host: %s, port %d", host, port)
- if form ==0 then
- return true, ("@%s:%d"):format(host,port)
- end
- -- for FORMAT_HOST_PORT_FACTORY, there's an object left to read
- local status, object = readObject0(dis)
- return true, ("@%s:%d"):format(host,port)
- --return true, {host = host, port = port, factory = object}
- else
- return doh("Invalid endpoint format")
- end
- end
- }
- --@see java.rmi.server.RemoteObject:readObject()
- ExternalClassParsers['java.rmi.server.RemoteObject'] = function(dis)
- local status, refClassName = dis:readUTF()
- if not status then return doh("Parsing external data, could not read classname (UTF)") end
- if #refClassName == 0 then
- local status, ref = readObject0(dis)
- return status, ref
- end
- dbg("Ref class name: %s ", refClassName)
- local parser = ExternalClassParsers[refClassName]
- if parser == nil then
- return doh("No external class reader for %s" , refClassName)
- end
- local status, object = parser(dis)
- return status, object
- end
- -- Attempts to parse the externalized data of an object.
- --@return status, the object data
- function parseExternalData(j_object)
- if j_object == nil then
- return doh("parseExternalData got nil object")
- end
- local className = j_object:getName()
- -- Find parser for the object, move up the hierarchy
- local obj = j_object
- local parser = nil
- while(className ~= nil) do
- parser = ExternalClassParsers[className]
- if parser ~= nil then break end
- obj = obj:getSuperClass()
- if obj== nil then break end-- No more super classes
- className = obj:getName()
- end
- if parser == nil then
- return doh("External reader for class %s is not implemented", tostring(className))
- end
- -- Read the actual object, start by creating a new dis based on the data-string
- local dis = JavaDIS:new(BufferedReader:new(nil,j_object.externalData))
- local status, object = parser(dis)
- if not status then
- return doh("Could not parse external data")
- end
- return true, object
- end
- -- Helper function to display data
- -- returns the string with all non-printable chars
- -- coded as hex
- function makeStringReadable(data)
- return data:gsub("[\x00-\x1f\x7f-\xff]", function (x)
- return ("\\x%02x"):format(x:byte())
- end)
- end
- function readNonProxyDesc(dis)
- dbg("-- entering readNonProxyDesc--")
- local j_class = JavaClass:new()
- local status, classname = dis:readUTF()
- if not status then return doh( "Could not read data" ) end
- j_class:setName(classname)
- local status, serialID = dis:readLongAsHexString()
- if not status then return doh("Could not read data") end
- j_class:setSerialID(serialID)
- dbg("Set serial ID to %s", tostring(serialID))
- local status, flags = dis:readByte()
- if not status then return doh("Could not read data") end
- j_class:setFlags(flags)
- local status, fieldCount = dis:readShort()
- if not status then return doh( "Could not read data") end
- dbg("Fieldcount %d", fieldCount)
- local fields = {}
- for i =0, fieldCount-1,1 do
- local status, fieldDesc = readFieldDesc(dis)
- j_class:addField(fieldDesc)
- -- Need to store in list, the field values need to be read
- -- after we have finished reading the class description
- -- hierarchy
- table.insert(fields,fieldDesc)
- end
- local status, customStrings = skipCustomData(dis)
- if status and customStrings ~= nil and #customStrings > 0 then
- j_class:setCustomData(customStrings)
- end
- local _,superDescriptor = readClassDesc(dis)
- j_class:setSuperClass(superDescriptor)
- dbg("Superclass read, now reading %i field values", #fields)
- --Read field values
- for i=1, #fields, 1 do
- local status, fieldType = dis:readByte()
- local value = nil
- if ( TypeDecoders[fieldType] ) then
- status, value= TypeDecoders[fieldType](dis)
- else
- dbg("error reading".. RMIUtils.tcString(fieldType))
- return
- end
- dbg("Read fieldvalue ".. tostring(value) .. " for field ".. tostring(fields[i]))
- fields[i]:setValue(value)
- end
- dbg("-- leaving readNonProxyDesc--")
- return true, j_class
- end
- function readProxyDesc(dis)
- dbg("-- in readProxyDesc--")
- local interfaces = ''
- local superclass = nil
- local status, ifaceNum= dis:readInt()
- if not status then return doh("Could not read data") end
- --dbg("# interfaces: %d" , ifaceNum)
- while ifaceNum > 0 do
- local status, iface = dis:readUTF()
- if not status then return doh( "Could not read data") end
- --table.insert(interfaces, iface)
- interfaces = interfaces .. iface ..', '
- dbg("Interface: %s " ,iface)
- ifaceNum = ifaceNum-1
- end
- local j_class = JavaClass:new()
- local status, customStrings = skipCustomData(dis)
- if status and customStrings ~= nil and #customStrings > 0 then
- j_class:setCustomData(customStrings)
- end
- local _,superDescriptor = readClassDesc(dis)
- --print ("superdescriptor", superDescriptor)
- j_class:setSuperClass(superDescriptor)
- j_class:setInterfaces(interfaces)
- dbg("-- leaving readProxyDesc--")
- return true, j_class
- end
- --
- -- Skips over all block data and objects until TC_ENDBLOCKDATA is
- -- encountered.
- -- @see java.io.ObjectInputStream.skipCustomData()
- --@return status
- --@return any strings found while searching
- function skipCustomData(dis)
- -- If we come across something interesting, just put it into
- -- the returnData list
- local returnData = {}
- while true do
- local status, p = dis:readByte()
- if not status then
- return doh("Could not read data")
- end
- if not status then return doh("Could not read data") end
- dbg("skipCustomData read %s", RMIUtils.tcString(p))
- if p == TC.TC_BLOCKDATA or p == TC.TC_BLOCKDATALONG then
- dbg("continuing")
- --return
- elseif p == TC.TC_ENDBLOCKDATA then
- return true, returnData
- else
- -- In the java impl, this is a function called readObject0. We just
- -- use the read null, otherwise error
- if p == TC.TC_NULL then
- -- No op, already read the byte, continue reading
- elseif p == TC.TC_STRING then
- --dbg("A string is coming!")
- local status, str = dis:readUTF()
- if not status then
- return doh("Could not read data")
- end
- dbg("Got a string, but don't know what to do with it! : %s",str)
- -- Object serialization is a bit messy. I have seen the
- -- classpath being sent over a customdata-field, so it is
- -- definitely interesting. Quick fix to get it showing
- -- is to just stick it onto the object we are currently at.
- -- So, just put the string into the returnData and continue
- table.insert(returnData, str)
- else
- return doh("Not implemented in skipcustomData:: %s", RMIUtils.tcString(p))
- end
- end
- end
- end
- function readFieldDesc(dis)
- -- fieldDesc:
- -- primitiveDesc
- -- objectDesc
- -- primitiveDesc:
- -- prim_typecode fieldName
- -- objectDesc:
- -- obj_typecode fieldName className1
- -- prim_typecode:
- -- `B' // byte
- -- `C' // char
- -- `D' // double
- -- `F' // float
- -- `I' // integer
- -- `J' // long
- -- `S' // short
- -- `Z' // boolean
- -- obj_typecode:
- -- `[` // array
- -- `L' // object
- local j_field = JavaField:new()
- local status, c = dis:readByte()
- if not status then return doh("Could not read data") end
- local char = string.char(c)
- local status, name = dis:readUTF()
- if not status then return doh("Could not read data") end
- local fieldType = ('primitive type: (%s) '):format(char)
- dbg("Fieldtype, char = %s, %s", tostring(fieldType), tostring(char))
- if char == 'L' or char == '[' then
- -- These also have classname which tells the type
- -- on the field
- local status, fieldclassname = readTypeString(dis)
- if not status then return doh("Could not read data") end
- if char == '[s' then
- fieldType = fieldclassname .. " []"
- else
- fieldType = fieldclassname
- end
- end
- if not status then
- return false, fieldType
- end
- dbg("Field description: name: %s, type: %s", tostring(name), tostring(fieldType))
- j_field:setType(fieldType)
- j_field:setName(name)
- -- setType = function( self, typ ) self.type = typ end,
- -- setSignature = function( self, sig ) self.signature = sig end,
- -- setName = function( self, name ) self.name = name end,
- -- setObjectType = function( self, ot ) self.object_type = ot end,
- -- setReference = function( self, ref ) self.ref = ref end,
- dbg("Created java field:".. tostring(j_field))
- return true, j_field
- end
- function readTypeString(dis)
- local status, tc = dis:readByte()
- if not status then return doh("Could not read data") end
- if tc == TC.TC_NULL then
- return true, nil
- elseif tc== TC.TC_REFERENCE then
- return doh("Not implemented, readTypeString(TC_REFERENCE)");
- elseif tc == TC.TC_STRING then
- return dis:readUTF()
- elseif tc == TC.TC_LONGSTRING then
- --TODO, add this (will throw error as is)
- return dis:readLongUTF()
- end
- end
- TypeDecoders =
- {
- [TC.TC_ARRAY] = readArray,
- [TC.TC_CLASSDESC] = readClassDesc,
- [TC.TC_STRING] = readString,
- [TC.TC_OBJECT] = readOrdinaryObject,
- }
- ---
- -- Registry
- -- Class to represent the RMI Registry.
- --@usage:
- -- registry = rmi.Registry:new()
- -- status, data = registry:list()
- Registry ={
- new = function (self,host, port)
- local o ={} -- create object
- setmetatable(o, self)
- self.__index = self -- DIY inheritance
- -- Hash code for sun.rmi.registry.RegistryImpl_Stub, which we are invoking :
- -- hex: 0x44154dc9d4e63bdf, dec: 4905912898345647071
- self.hash = '44154dc9d4e63bdf'
- -- RmiRegistry object id is 0
- self.objId = 0
- o.host = host
- o.port = port
- return o
- end
- }
- -- Connect to the remote registry.
- --@return status
- --@return error message
- function Registry:_handshake()
- local out = RmiDataStream:new()
- local status, err = out:connect(self.host,self.port)
- if not status then
- return doh("Registry connection failed: %s", tostring(err))
- end
- dbg("Registry connection OK "..tostring(out.bsocket) )
- self.out = out
- return true
- end
- ---
- -- List the named objects in the remote RMI registry
- --@return status
- --@return a table of strings , or error message
- function Registry:list()
- if not self:_handshake() then
- return doh("Handshake failed")
- end
- -- Method list() is op number 1
- return self.out:invoke(self.objId, self.hash,1)
- end
- ---
- -- Perform a lookup on an object in the Registry,
- -- takes the name which is bound in the registry
- -- as argument
- --@return status
- --@return JavaClass-object
- function Registry:lookup(name)
- self:_handshake()
- -- Method lookup() is op number 2
- -- Takes a string as arguments
- local a = Arguments:new()
- a:addString(name)
- return self.out:invoke(self.objId, self.hash,2, a)
- end
- ----
- -- Arguments class
- -- This class is meant to handle arguments which is sent to a method invoked
- -- remotely. It is mean to contain functionality to add java primitive datatypes,
- -- such as pushInt, pushString, pushLong etc. All of these are not implemented
- -- currently
- --@usage: When invoking a remote method
- -- use this class in this manner:
- -- Arguments a = Arguments:new()
- -- a:addString("foo")
- -- datastream:invoke{objNum=oid, hash=hash, opNum = opid, arguments=a}
- -- ...
- --
- Arguments = {
- new = function (self,o)
- o = o or {} -- create object if user does not provide one
- setmetatable(o, self)
- self.__index = self -- DIY inheritance
- -- We use a buffered socket just to be able to use a javaDOS for writing
- self.dos = JavaDOS:new(BufferedWriter:new())
- return o
- end,
- addString = function(self, str)
- self.dos:writeByte(TC.TC_STRING)
- self.dos:writeUTF(str)
- end,
- addRaw = function(self, str)
- self.dos:write(str)
- end,
- getData = function(self)
- local _, res = self.dos:flush()
- return res
- end
- }
- ---
- -- RMIUtils class provides some some codes and definitions from Java
- -- There are three types of output messages: Call, Ping and DgcAck.
- -- A Call encodes a method invocation. A Ping is a transport-level message
- -- for testing liveness of a remote virtual machine.
- -- A DGCAck is an acknowledgment directed to a
- -- server's distributed garbage collector that indicates that remote objects
- -- in a return value from a server have been received by the client.
- RMIUtils = {
- -- Indicates a Serializable class defines its own writeObject method.
- SC_WRITE_METHOD = 0x01,
- -- Indicates Externalizable data written in Block Data mode.
- SC_BLOCK_DATA = 0x08,
- -- Bit mask for ObjectStreamClass flag. Indicates class is Serializable.
- SC_SERIALIZABLE = 0x02,
- --Bit mask for ObjectStreamClass flag. Indicates class is Externalizable.
- SC_EXTERNALIZABLE = 0x04,
- --Bit mask for ObjectStreamClass flag. Indicates class is an enum type.
- SC_ENUM = 0x10,
- flagsToString = function(flags)
- local retval = ''
- if ( bit.band(flags, RMIUtils.SC_WRITE_METHOD) ~= 0) then
- retval = retval .. " WRITE_METHOD"
- end
- if ( bit.band(flags, RMIUtils.SC_BLOCK_DATA) ~= 0) then
- retval = retval .. " BLOCK_DATA"
- end
- if ( bit.band(flags, RMIUtils.SC_EXTERNALIZABLE) ~= 0) then
- retval = retval .. " EXTERNALIZABLE"
- end
- if ( bit.band(flags, RMIUtils.SC_SERIALIZABLE) ~= 0) then
- retval = retval .. " SC_SERIALIZABLE"
- end
- if ( bit.band(flags, RMIUtils.SC_ENUM) ~= 0) then
- retval = retval .. " SC_ENUM"
- end
- return retval
- end,
- tcString = function (constant)
- local x = TC.Strings[constant] or "Unknown code"
- return ("%s (0x%x)"):format(x,tostring(constant))
- end,
- }
- local RMIMessage = {
- Call = 0x50,
- Ping = 0x52,
- DgcAck= 0x54,
- }
- STREAM_MAGIC = 0xaced
- STREAM_VERSION = 5
- baseWireHandle = 0x7E0000
- return _ENV;