LLUDP Dissector

From OpenSimulator

Revision as of 12:26, 27 February 2009 by Neopallium (Talk | contribs)

Jump to: navigation, search

Contents

LLUDP protocol dissector

On this page you will find the Lua code for a wireshark protocol dissector that can parse the message_template.msg file and use that information to decode all the message fields from the Linden UDP protocol.

Installing

on Linux

  • Copy all four source files into ~/.wireshark
  • edit /etc/wireshark/init.lua (or equivalent on your system) and change disable_lua to false (default is true)
  • If you need to run wireshark as the root user or using sudo then you will need to edit the scripts into one file by replacing the dofile("script.lua") calls with the contents of file between the quotes.
  • The other method is to add your user account to the correct group (on Gentoo it is group "wireshark") that will allow your non-root user to capture packets.

on Windows

  • Copy all four source files into your user profiles directory

Vista*

   C:\Users\<username>\AppData\Roaming\Wireshark 

XP/2000

   C:\Documents and Settings\<username>\Application Data\Wireshark
  • Edit C:\Program Files\Wireshark\init.lua and change disable_lua to false (default is true)
  • *Note: I have only tested this on Windows XP

LLUDP preferences

There are three preferences that can be changed from wiresharks "Preferences" dialog:

  • Message template file: Full path to the message_template.msg file used to decode message name & details from the packets. On windows XP/Vista use double backslash '\\' instead of single blackslash '\' to separate directories (Example "C:\\Program Files\\SecondLife\\app_settings\\message_template.msg").
  • UDP port range start: First UDP port to mark as LLUDP packets. (default 13000)
  • UDP port range end: Last UDP port to mark as LLUDP packets. (default 13050)

If your OpenSim regions are using ports 9000-9050 range then change the UDP port range.

Description of source files

  • "init.lua" -- simple script that loads the "lludp.lua" script.
  • "lludp.lua" -- contains the code that decodes each packet header and decompresses zero-encoded packets. This file uses wireshark only functions for accessing packet bytes and building a tree of information from each packet.
  • "llmessage.lua" -- contains the message_template.msg file parser the decodes the tokens from the lexer into an tree of tables containing all details about each message/block/variable from the template file. This file only has pure lua code.
  • "lexer.lua" -- contains the template file lexer. This lexer knows how to tokenize the template file into the follow tokens: IDENTIFIER, NUMBER, COMMENT, EOL. The stream of tokens produced by this lexer is parsed by the "llmessage.lua" file. This file only has pure lua code.


File "init.lua"

-- register http to handle tcp ports 8000-8010 and 9000
do
  local tcp_port_table = DissectorTable.get("tcp.port")
  local http_dissector = tcp_port_table:get_dissector(80)
  for port = 8000,8010 do
    tcp_port_table:add(port,http_dissector)
  end
  tcp_port_table:add(9000,http_dissector)
end
 
-- Load lludp protocol dissector.
dofile("lludp.lua")

File "lludp.lua"

--[[
BSD-Licensed:
Copyright (c) 2008, Robert G. Jakabosky <bobby@sharedrealm.com>
All rights reserved.
 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 
    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the SharedRealm nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
]]
 
dofile("llmessage.lua")
 
-- cache globals to local for speed.
local str_format=string.format
 
-- test if ByteArray only has printable ASCII character and ends with '\0'
local allowed_special = {
	[0] = true, -- null
	[9] = true, -- tab
	[10] = true, -- new line
	[13] = true, -- carriage return
}
local function is_string(bytes)
	local c
	local max = bytes:len() - 1
	for i=0,max do
		c = bytes:get_index(i)
		if c >= 127 then -- not ascii character
			return false
		elseif c < 32 then  -- control character range
			if not allowed_special[c] then
				-- control characters between NULL and Space
				return false
			elseif c == 0 and i < max then
				-- null byte only allowed at end.
				return false
			end
		end
	end
	return true
end
 
-- lludp protocol example
-- declare our protocol
local lludp_proto = Proto("lludp","LLUDP","LindenLabs UDP Protocol")
 
-- setup preferences
lludp_proto.prefs["template_file"] =
	Pref.string("Message template file", "message_template.msg", "Message template file")
lludp_proto.prefs["udp_port_start"] =
	Pref.string("UDP port range start", "13000", "First UDP port to decode as this protocol")
lludp_proto.prefs["udp_port_end"] =
	Pref.string("UDP port range end", "13050", "Last UDP port to decode as this protocol")
-- current preferences settings.
local current_settings = {
template_file = "",
udp_port_start = -1,
udp_port_end = -1,
}
-- current list of parsed messages.
local message_details = nil
 
-- setup protocol fields.
lludp_proto.fields = {}
local fds = lludp_proto.fields
fds.flags = ProtoField.new("Flags", "lludp.flags", "FT_UINT8", nil, "BASE_HEX", "0xFF")
fds.flags_zero = ProtoField.new("Zero", "lludp.flags.zero", "FT_UINT8", nil, "BASE_HEX", "0x80")
fds.flags_reliable = ProtoField.new("Reliable", "lludp.flags.rel", "FT_UINT8", nil, "BASE_HEX", "0x40")
fds.flags_resent = ProtoField.new("Resent", "lludp.flags.res", "FT_UINT8", nil, "BASE_HEX", "0x20")
fds.flags_ack = ProtoField.new("Ack", "lludp.flags.ack", "FT_UINT8", nil, "BASE_HEX", "0x10")
fds.sequence = ProtoField.new("Sequence", "lludp.sequence", "FT_UINT32", nil, "BASE_DEC")
fds.extra_len = ProtoField.new("Extra length", "lludp.extra_len", "FT_UINT8", nil, "BASE_DEC")
fds.extra_bytes = ProtoField.new("Extra header", "lludp.extra_bytes", "FT_BYTES", nil, "BASE_HEX")
fds.msg_id = ProtoField.new("Message ID", "lludp.msg.id", "FT_UINT32", nil, "BASE_HEX")
fds.msg_name = ProtoField.new("Message name", "lludp.msg.name", "FT_STRINGZ", nil)
fds.msg = ProtoField.new("Message body", "lludp.msg", "FT_BYTES", nil, "BASE_HEX")
fds.acks_count = ProtoField.new("Acks count", "lludp.acks_count", "FT_UINT8", nil, "BASE_DEC")
fds.acks = ProtoField.new("Acks", "lludp.acks", "FT_UINT32", nil, "BASE_DEC")
fds.block_count = ProtoField.new("Block count", "lludp.block_count", "FT_UINT8", nil, "BASE_DEC")
fds.block = ProtoField.new("Block", "lludp.block", "FT_BYTES", nil, "BASE_HEX")
fds.var_fixed = ProtoField.new("Fixed blob", "lludp.var.fixed", "FT_BYTES", nil, "BASE_HEX")
fds.var_variable = ProtoField.new("Variable blob", "lludp.var.variable", "FT_BYTES", nil, "BASE_HEX")
fds.var_string = ProtoField.new("String", "lludp.var.string", "FT_STRINGZ", nil)
fds.var_u8 = ProtoField.new("U8", "lludp.var.u8", "FT_UINT8", nil, "BASE_DEC")
fds.var_u16 = ProtoField.new("U16", "lludp.var.u16", "FT_UINT16", nil, "BASE_DEC")
fds.var_u32 = ProtoField.new("U32", "lludp.var.u32", "FT_UINT32", nil, "BASE_DEC")
fds.var_u64 = ProtoField.new("U64", "lludp.var.u64", "FT_UINT64", nil, "BASE_DEC")
fds.var_s8 = ProtoField.new("S8", "lludp.var.s8", "FT_INT8", nil, "BASE_DEC")
fds.var_s16 = ProtoField.new("S16", "lludp.var.s16", "FT_INT16", nil, "BASE_DEC")
fds.var_s32 = ProtoField.new("S32", "lludp.var.s32", "FT_INT32", nil, "BASE_DEC")
fds.var_s64 = ProtoField.new("S64", "lludp.var.s64", "FT_INT64", nil, "BASE_DEC")
fds.var_f32 = ProtoField.new("F32", "lludp.var.f32", "FT_FLOAT", nil, "BASE_DEC")
fds.var_f64 = ProtoField.new("F64", "lludp.var.f64", "FT_DOUBLE", nil, "BASE_DEC")
fds.var_llvector3 = ProtoField.new("LLVector3", "lludp.var.llvector3", "FT_BYTES", nil, "BASE_HEX")
fds.var_llvector3d = ProtoField.new("LLVector3d", "lludp.var.llvector3d", "FT_BYTES", nil, "BASE_HEX")
fds.var_llvector4 = ProtoField.new("LLVector4", "lludp.var.llvector4", "FT_BYTES", nil, "BASE_HEX")
fds.var_llquaternion = ProtoField.new("LLQuaternion", "lludp.var.llquaternion", "FT_BYTES", nil, "BASE_HEX")
fds.var_lluuid = ProtoField.new("LLUUID", "lludp.var.lluuid", "FT_BYTES", nil, "BASE_HEX")
fds.var_bool = ProtoField.new("BOOL", "lludp.var.bool", "FT_UINT8", nil, "BASE_DEC")
fds.var_ipaddr = ProtoField.new("IPADDR", "lludp.var.ipaddr", "FT_IPv4", nil, "BASE_DEC")
fds.var_ipport = ProtoField.new("IPPORT", "lludp.var.ipport", "FT_UINT16", nil, "BASE_DEC")
-- variable type handlers.
local variable_handlers = {
Fixed = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_fixed, rang)
	if len <= 4 then
		ti:set_text(str_format("%s: 0x%08x",var.name, rang:uint()))
	else
		ti:set_text(str_format("%s: length=%d, Blob:%s", var.name, len, tostring(rang)))
	end
end,
Variable = function(block_tree, buffer, offset, len, var)
	local is_data = false
	-- try to guess if this field is text.
	if var.name:find("Data") then
		-- this is a data find.
		is_data = true
	end
	local str_rang = buffer(offset + var.count_length, len - var.count_length)
	local bytes = str_rang:bytes()
	if not is_data and is_string(bytes) then
		local str = str_rang:string()
		local ti = block_tree:add(fds.var_string, buffer(offset, len), str)
		ti:set_text(str_format("%s: %s", var.name, str))
	else
		local rang = buffer(offset, len)
		local ti = block_tree:add_le(fds.var_variable, rang)
		if len <= 4 then
			ti:set_text(str_format("%s: 0x%08x",var.name, rang:uint()))
		else
			ti:set_text(str_format("%s: length=%d, Blob:%s", var.name, len, tostring(rang)))
		end
	end
end,
U8 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_u8, rang)
	ti:set_text(str_format("%s: %d", var.name, rang:le_uint()))
end,
U16 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_u16, rang)
	ti:set_text(str_format("%s: %d", var.name, rang:le_uint()))
end,
U32 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_u32, rang)
	ti:set_text(str_format("%s: %d", var.name, rang:le_uint()))
end,
U64 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_u64, rang)
	ti:set_text(str_format("%s: 0x%s",var.name, tostring(rang)))
end,
S8 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_s8, rang)
	local num = rang:le_uint()
	if num > 127 then num = num - 256 end
	ti:set_text(str_format("%s: %d",var.name, num))
end,
S16 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_s16, rang)
	local num = rang:le_uint()
	if num > 32768 then num = num - 65536 end
	ti:set_text(str_format("%s: %d",var.name, num))
end,
S32 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_s32, rang)
	local num = rang:le_uint()
	if num > 2147483648 then num = num - 4294967296 end
	ti:set_text(str_format("%s: %d",var.name, num))
end,
S64 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_s64, rang)
	ti:set_text(str_format("%s: 0x%s",var.name, tostring(rang)))
end,
F32 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_f32, rang)
	ti:set_text(str_format("%s: %f", var.name, rang:le_float()))
end,
F64 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_f64, rang)
	ti:set_text(str_format("%s: %f", var.name, rang:le_float()))
end,
LLVector3 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_llvector3, rang)
	-- parse LLVector3
	local x,y,z
	x = buffer(offset + 0,4):le_float()
	y = buffer(offset + 4,4):le_float()
	z = buffer(offset + 8,4):le_float()
	-- display
	ti:set_text(str_format("%s: <%f,%f,%f>", var.name, x, y, z))
end,
LLVector3d = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_llvector3d, rang)
	-- parse LLVector3d
	local x,y,z
	x = buffer(offset + 0,8):le_float()
	y = buffer(offset + 8,8):le_float()
	z = buffer(offset + 16,8):le_float()
	-- display
	ti:set_text(str_format("%s: <%f,%f,%f>", var.name, x, y, z))
end,
LLVector4 = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_llvector4, rang)
	-- parse LLVector4
	local x,y,z,s
	x = buffer(offset + 0,4):le_float()
	y = buffer(offset + 4,4):le_float()
	z = buffer(offset + 8,4):le_float()
	s = buffer(offset + 12,4):le_float()
	-- display
	ti:set_text(str_format("%s: <%f,%f,%f,%f>", var.name, x, y, z, s))
end,
LLQuaternion = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_llquaternion, rang)
	-- parse LLQuaternion
	local x,y,z,w
	x = buffer(offset + 0,4):le_float()
	y = buffer(offset + 4,4):le_float()
	z = buffer(offset + 8,4):le_float()
	-- calculate W
	w = 1 - (x * x) - (y * y) - (z * z)
	if w > 0 then
		w = math.sqrt(w)
	else
		w = 0
	end
	-- display
	ti:set_text(str_format("%s: <%f,%f,%f,%f>", var.name, x, y, z, w))
end,
LLUUID = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_lluuid, rang)
	local str = tostring(rang)
	str = str:sub(1,8) .. '-' ..
		str:sub(9,12) .. '-' .. str:sub(13,16) .. '-' ..
		str:sub(17,20) .. '-' .. str:sub(21)
	ti:set_text(str_format("%s: %s", var.name, str))
end,
BOOL = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add_le(fds.var_bool, rang)
	local val = "false"
	if rang:le_uint() > 0 then
		val = "true"
	end
	ti:set_text(str_format("%s: %s", var.name, val))
end,
IPADDR = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add(fds.var_ipaddr, rang)
	ti:set_text(str_format("%s: %s", var.name, tostring(rang:ipv4())))
end,
IPPORT = function(block_tree, buffer, offset, len, var)
	local rang = buffer(offset, len)
	local ti = block_tree:add(fds.var_ipport, rang)
	ti:set_text(str_format("%s: %d", var.name, rang:uint()))
end,
}
 
-- un-register lludp to handle udp port range
local function unregister_udp_port_range(start_port, end_port)
	if not start_port or start_port <= 0 or not end_port or end_port <= 0 then
		return
	end
  udp_port_table = DissectorTable.get("udp.port")
  for port = start_port,end_port do
    udp_port_table:remove(port,lludp_proto)
  end
end
 
-- register lludp to handle udp port range
local function register_udp_port_range(start_port, end_port)
	if not start_port or start_port <= 0 or not end_port or end_port <= 0 then
		return
	end
  udp_port_table = DissectorTable.get("udp.port")
  for port = start_port,end_port do
    udp_port_table:add(port,lludp_proto)
  end
end
 
-- handle preferences changes.
function lludp_proto.init(arg1, arg2)
	local old_start, old_end
	local new_start, new_end
	-- check if preferences have changed.
	for pref_name,old_v in pairs(current_settings) do
		local new_v = lludp_proto.prefs[pref_name]
		if new_v ~= old_v then
			if pref_name == "template_file" then
				-- load & parse message_template.msg file.
				local file = new_v
				if file and file:len() > 0 then
					local new_details = parse_template(file)
					if new_details then
						message_details = new_details
					end
				end
			elseif pref_name == "udp_port_start" then
				old_start = old_v
				new_start = new_v
			elseif pref_name == "udp_port_end" then
				old_end = old_v
				new_end = new_v
			end
			-- save new value.
			current_settings[pref_name] = new_v
		end
	end
	-- un-register old port range
	if old_start and old_end then
		unregister_udp_port_range(tonumber(old_start), tonumber(old_end))
	end
	-- register new port range.
	if new_start and new_end then
		register_udp_port_range(tonumber(new_start), tonumber(new_end))
	end
end
 
-- parse flag bits.
local FLAG_ZER = 4
local FLAG_REL = 3
local FLAG_RES = 2
local FLAG_ACK = 1
local flag_names = {"ACK", "RES", "REL", "ZER"}
local bits_lookup = {
	{},
	{1},
	{2},
	{2,1},
	{3},
	{3,1},
	{3,2},
	{3,2,1},
	{4},
	{4,1},
	{4,2},
	{4,2,1},
	{4,3},
	{4,3,1},
	{4,3,2},
	{4,3,2,1},
}
local function parse_flags(flags)
	flags = (flags / 16) + 1
	local bit_list = bits_lookup[flags]
	local bits = {}
	local names = ""
	for _, bit in ipairs(bit_list) do
		bits[bit] = true
		if names:len() > 0 then
			names = names .. ", "
		end
		names = names .. flag_names[bit]
	end
	return bits, names
end
 
local function grow_buff(buff, size)
	local old_size = buff:len()
	if old_size > size then return end
	-- buffer needs to grow
	buff:set_size(size)
	-- fill new space with zeros
	for i = old_size,size-1 do
		buff:set_index(i, 0)
	end
end
 
local function zero_decode(zero_buf)
	local out_buf = ByteArray.new()
	local zero_off = 0
	local zero_len = zero_buf:len()
	local out_size = 0
	local out_off = 0
	local b
	-- pre-allocate
	grow_buff(out_buf, zero_len)
	out_size = zero_len
	-- zero expand
	repeat
		b = zero_buf:get_index(zero_off)
		if b == 0 then
			-- get zero count
			local count = zero_buf:get_index(zero_off + 1)
			if count == 0 then count = 255 end
			out_off = out_off + count
			-- fill zeros
			if out_off > out_size then
				out_size = out_off + 128
				grow_buff(out_buf, out_size)
			end
			zero_off = zero_off + 2
		else
			if out_off >= out_size then
				out_size = out_off + (zero_len - zero_off) + 4
				grow_buff(out_buf, out_size)
			end
			-- copy non-zero bytes.
			out_buf:set_index(out_off,b)
			zero_off = zero_off + 1
			out_off = out_off + 1
		end
	until zero_off == zero_len
	-- truncate to real size.
	out_buf:set_size(out_off)
 
	return out_buf:tvb("Decompressed Data")
end
 
local function parse_msg_id(buff)
	local b = buff:get_index(0)
	local msg_id = b
	local msg_id_len = 1
	if b == 255 then
		b = buff:get_index(1)
		msg_id = msg_id * 256 + b
		msg_id_len = 2
		if b == 255 then
			b = buff:get_index(2)
			msg_id = msg_id * 256 + b
			b = buff:get_index(3)
			msg_id = msg_id * 256 + b
			msg_id_len = 4
		end
	end
	return msg_id, msg_id_len
end
 
-- get message name.
local function get_msg_name(msg_id)
	-- check that we have message details
	if message_details == nil then
		return str_format("0x%08x", msg_id)
	end
	-- find message name from id.
	local msg = message_details.msgs[msg_id]
	-- Invalid message id
	if msg == nil then
		return str_format("0x%08x", msg_id)
	end
	return msg.name
end
 
-- calculate length a block.
local function get_block_length(msg_buffer, start_offset, block)
	-- check if bock is fixed length.
	if block.fixed_length then
		return block.min_length
	end
	-- parse block's variables to calculate total block length.
	local offset = start_offset
	local rang
	for _,var in ipairs(block) do
		local len = 0
		if var.has_count then
			-- variable with length bytes.
			len = var.count_length
			--print(var.name, offset, ", len:", len)
			rang = msg_buffer(offset, len)
			len = len + rang:le_uint()
			--print(var.name, var.count_length, ", total:", len)
		else
			-- fixed length variable
			len = var.length
			--print(var.name, ", total:", len)
		end
		offset = offset + len
	end
	return (offset - start_offset)
end
 
-- build block tree
local function build_block_tree(msg_buffer, block_tree, start_offset, block)
	local offset = start_offset
	local rang
	-- parse block's variables
	for _,var in ipairs(block) do
		local len = 0
		if var.has_count then
			-- variable with length bytes.
			len = var.count_length
			rang = msg_buffer(offset, len)
			len = len + rang:le_uint()
		else
			-- fixed length variable
			len = var.length
		end
		-- get variable's type field.
		local handler = variable_handlers[var.type]
		-- parse variable.
		if handler then
			handler(block_tree, msg_buffer, offset, len, var)
		end
		offset = offset + len
	end
	return (offset - start_offset)
end
 
-- buid message tree
local function build_msg_tree(msg_buffer, msg_tree, msg_id)
	local offset = 0
	local rang
	-- check that we have message details
	if message_details == nil then
		msg_tree:set_text(str_format("Message Id: 0x%08x", msg_id))
		return nil
	end
	-- find message name from id.
	local msg = message_details.msgs[msg_id]
	-- Invalid message id
	if msg == nil then
		msg = str_format("Invalid message id: 0x%08x", msg_id)
		msg_tree:add_expert_info(PI_MALFORMED, PI_ERROR, msg)
		msg_tree:set_text(msg)
		return nil
	end
	-- skip message id bytes.
	offset = msg.id_length
  -- set message name.
	msg_tree:set_text(msg.name .. ":")
	-- proccess message blocks
	for _,block in ipairs(msg) do
		local count = block.count
		if count == nil then
			-- parse count byte.
			rang = msg_buffer(offset,1)
			count = rang:uint()
  		msg_tree:add(fds.block_count,rang)
			offset = offset + 1
		end
		-- print("block name: ", block.name, count)
		for n=1,count do
			local block_len = get_block_length(msg_buffer, offset, block)
			-- parse block
			rang = msg_buffer(offset, block_len)
  		local block_tree = msg_tree:add(fds.block,rang)
			if count > 1 then
				block_tree:set_text(str_format("%s: %d of %d",block.name,n,count))
			else
				block_tree:set_text(block.name)
			end
			-- parse block variables.
			build_block_tree(msg_buffer, block_tree, offset, block)
			offset = offset + block_len
		end
	end
	return msg.name
end
 
-- packet dissector
function lludp_proto.dissector(buffer,pinfo,tree)
	local rang,offset
  pinfo.cols.protocol = "LLUDP"
  local lludp_tree = tree:add(lludp_proto,buffer(),"Linden UDP Protocol")
	-- Flags byte.
	offset = 0
	rang = buffer(offset,1)
	local flags = rang:uint()
	local flags_bits, flags_list = parse_flags(flags)
  flags_tree = lludp_tree:add(fds.flags, rang)
	flags_tree:set_text("Flags: " .. str_format('0x%02X (%s)', flags, flags_list))
  flags_tree:add(fds.flags_zero, rang)
  flags_tree:add(fds.flags_reliable, rang)
  flags_tree:add(fds.flags_resent, rang)
  flags_tree:add(fds.flags_ack, rang)
	offset = offset + 1
	-- Sequence number 4 bytes.
	rang = buffer(offset,4)
	local sequence = rang:uint()
  lludp_tree:add(fds.sequence, rang)
	offset = offset + 4
	-- Extra header length.
	rang = buffer(offset,1)
	local extra_length = rang:uint()
  lludp_tree:add(fds.extra_len,rang)
	offset = offset + 1
	-- Extra header data.
	if extra_length > 0 then
		rang = buffer(offset, extra_length)
		lludp_tree:add(fds.extra_bytes, rang)
		offset = offset + extra_length
	end
	-- Appended Acks. count
	local acks_bytes = 0
	local acks_count = 0
	if flags_bits[FLAG_ACK] then
		rang = buffer(buffer:len() - 1, 1)
		acks_count = rang:uint()
		acks_bytes = (acks_count * 4) + 1
	end
	-- Zero Decode
	local msg_len = (buffer:len() - acks_bytes) - offset
	if flags_bits[FLAG_ZER] then
		msg_buffer=zero_decode(buffer(offset,msg_len):bytes())
		msg_len = msg_buffer:len()
		offset = 0
	else
		msg_buffer = buffer(offset, msg_len):tvb()
		offset = 0
	end
	-- Message ID
	local msg_id, msg_id_len = -1, 4
	if msg_id_len > msg_len then
		msg_id_len = msg_len
	end
	msg_id, msg_id_len = parse_msg_id(msg_buffer(offset, msg_id_len):bytes())
	rang = msg_buffer(offset, msg_id_len)
	lludp_tree:add(fds.msg_id, rang)
	local msg_name = get_msg_name(msg_id)
	if msg_name == nil then
		msg_name = str_format("0x%08x", msg_id)
	else
		lludp_tree:add(fds.msg_name, rang, msg_name)
	end
	-- Message body.
	rang = msg_buffer(offset, msg_len)
	local msg_tree = lludp_tree:add(fds.msg, rang)
	build_msg_tree(msg_buffer, msg_tree, msg_id)
	-- Appended Acks. list.
	if flags_bits[FLAG_ACK] then
		local acks_off = buffer:len()
		rang = buffer(acks_off - 1, 1)
		acks_off = acks_off - acks_bytes
  	local acks_tree = lludp_tree:add(fds.acks_count, rang)
		for i = 1,acks_count do
			rang = buffer(acks_off,4)
  		acks_tree:add(fds.acks, rang)
			acks_off = acks_off + 4
		end
	end
	-- Info column
  pinfo.cols.info = str_format('[%s] Seq=%u Type=%s', flags_list, sequence, msg_name)
end
 
-- register lludp to handle udp ports 9000-9003
register_udp_port_range(9000,9003)

File "llmessage.lua"

--[[
BSD-Licensed:
Copyright (c) 2008, Robert G. Jakabosky <bobby@sharedrealm.com>
All rights reserved.
 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 
    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the SharedRealm nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
]]
 
dofile("lexer.lua")
 
local lexer
local cur_token = nil
local cur_token_str = nil
 
local function get_token(skip_tokens)
	repeat
	token = lexer.get_token()
	if token ~= nil then
		cur_token = token[1]
		cur_token_str = token[2]
	end
	until token == nil or not skip_tokens[cur_token]
	return token
end
 
local function run_parser(parser)
	local state = parser.init()
	if parser.skip_tokens == nil then parser.skip_tokens = {} end
	while get_token(parser.skip_tokens) do
		-- check what the parser is expecting next.
		if state.expect then
			-- check expected type
			if state.expect ~= cur_token then
				error(string.format("state.expected token '%s' instead of '%s'",
					TokenNames[state.expect], TokenNames[cur_token]))
			end
			-- reset expect field
			state.expect = nil
		end
		if state.expect_str then
			-- check expected string
			if state.expect_str ~= cur_token_str then
				error(string.format("state.expected token '%s' instead of '%s'",
					state.expect_str, cur_token_str))
			end
			-- reset expect_str field
			state.expect_str = nil
		end
		-- get handler function for current token.
		local f = parser[cur_token]
		if f ~= nil then
			local ret = f(state)
			if ret then
				-- praser finished.
				return ret
			end
		elseif parser.unhandled_error then
			error(string.format("unhandled token '%s' when paring '%s'\n", cur_token_str, parser.name))
		end
	end
	return parser.eof(state)
end
 
-- Known variable types and there fixed length.
--   length == -1, requires a number after the type that is the length of count field
--   length == -2, requires a number after the type that is the fixed variable length.
VariableTypes = {
Null = 0,
Fixed = -2,
Variable = -1,
U8 = 1,
U16 = 2,
U32 = 4,
U64 = 8,
S8 = 1,
S16 = 2,
S32 = 4,
S64 = 8,
F32 = 4,
F64 = 8,
LLVector3 = 12,
LLVector3d = 24,
LLVector4 = 16,
LLQuaternion = 12,
LLUUID = 16,
BOOL = 1,
IPADDR = 4,
IPPORT = 2,
}
 
--
-- Variable parser
--
local variable_parser = {
name = "variable",
unhandled_error = false,
skip_tokens = {[Token.EOL] = true},
init = function()
	return {
		name = "<MISSING VARIABLE NAME>",
		type = "Null",
		has_count = false,
		length = 0,
		expect = Token.IDENTIFIER,
		expect_field = "name",
		required = 2
	}
end,
[Token.IDENTIFIER] 	= function(state)
	if state.expect_field == "name" then
		state.name = cur_token_str
		state.expect = Token.IDENTIFIER
		state.expect_field = "type"
		state.required = state.required - 1
	elseif state.expect_field == "type" then
		state.type = cur_token_str
		state.length = VariableTypes[state.type]
		if state.length == nil then
			error("Unknown variable type: " .. cur_token_str)
		elseif state.length == -1 or state.length == -2 then
			state.expect = Token.NUMBER
		else
			state.required = state.required - 1
		end
	else
		error(string.format("unhandled variable identifier: %s\n",cur_token_str))
	end
	return nil
end,
[Token.NUMBER]			= function(state)
	if state.expect_field == "type" then
		if state.length == -1 then
			-- variable field length uses embedded count field
			state.has_count = true
			state.count_length = tonumber(cur_token_str)
			state.length = nil
		elseif state.length == -2 then
			-- fixed field length
			state.length = tonumber(cur_token_str)
		end
		state.required = state.required - 1
	else
		error(string.format("unhandled variable number: %s\n",cur_token_str))
	end
	return nil
end,
["{"]		= function(state)
	error("sub block not allowed in variable block")
end,
["}"]		= function(state)
	if state.required > 0 then
		error("missing " .. state.required .. " fields")
	end
	-- clean state.
	state.required = nil
	state.expect = nil
	state.expect_field = nil
	return state
end,
eof = function(state)
	error("missing '}' token at end of variable: " .. state.name)
end,
}
 
-- Block Quantities
local BlockQuantity = {
Single = 1,
Variable = -1,
Multiple = -2,
}
 
--
-- Block parser
--
local block_parser = {
name = "block",
unhandled_error = false,
skip_tokens = {[Token.EOL] = true},
init = function()
	return {
		name = "<MISSING BLOCK NAME>",
		quantity = "Single",
		count = 0,
		min_length = 0,
		fixed_length = true,
		expect = Token.IDENTIFIER,
		expect_field = "name",
		required = 2
	}
end,
[Token.IDENTIFIER] 	= function(state)
	if state.expect_field == "name" then
		state.name = cur_token_str
		state.expect = Token.IDENTIFIER
		state.expect_field = "quantity"
		state.required = state.required - 1
	elseif state.expect_field == "quantity" then
		state.quantity = cur_token_str
		state.count = BlockQuantity[cur_token_str]
		if state.count == nil then
			error("Unknown block quantity: " .. cur_token_str)
		elseif state.count == -2 then
			state.expect_field = "count"
			state.expect = Token.NUMBER
		else
			if state.count == -1 then
				state.has_count = true
				state.count_length = 1
				state.count = nil
			end
			state.required = state.required - 1
		end
	else
		error(string.format("unhandled block identifier: %s\n",cur_token_str))
	end
	return nil
end,
[Token.NUMBER]			= function(state)
	if state.expect_field == "count" then
		state.count = tonumber(cur_token_str)
		state.required = state.required - 1
	else
		error(string.format("unhandled block number: %s\n",cur_token_str))
	end
	return nil
end,
["{"]		= function(state)
	local variable = run_parser(variable_parser)
	table.insert(state,variable)
	-- add length of fixed length variables to minimal length of block.
	if variable.has_count then
		state.min_length = state.min_length + variable.count_length
		state.fixed_length = false
	else
		state.min_length = state.min_length + variable.length
	end
end,
["}"]		= function(state)
	if state.required > 0 then
		error("missing " .. state.required .. " fields")
	end
	-- clean state.
	state.required = nil
	state.expect = nil
	state.expect_field = nil
	return state
end,
eof = function(state)
	error("missing '}' token at end of block: " .. state.name)
end,
}
 
--
-- Message parser
--
local message_parser = {
name = "message",
unhandled_error = false,
skip_tokens = {[Token.EOL] = true},
init = function()
	-- create state
	return {
		name = "<MISSING MESSAGE NAME>",
		expect = Token.IDENTIFIER,
		expect_field = "name",
		fixed_length = true,
		min_length = 0,
		required = 5
	}
end,
[Token.IDENTIFIER] 	= function(state)
	if state.expect_field == "name" then
		state.name = cur_token_str
		state.expect = Token.IDENTIFIER
		state.expect_field = "frequency"
		state.required = state.required - 1
	elseif state.expect_field == "frequency" then
		state.frequency = cur_token_str
		state.expect = Token.NUMBER
		state.required = state.required - 1
	elseif state.expect_field == "trust" then
		state.trust = cur_token_str
		state.expect = Token.IDENTIFIER
		state.expect_field = "compression"
		state.required = state.required - 1
	elseif state.expect_field == "compression" then
		state.compression = cur_token_str
		state.required = state.required - 1
	else
		error(string.format("unhandled message identifier: %s\n",cur_token_str))
	end
	return nil
end,
[Token.NUMBER]			= function(state)
	if state.expect_field == "frequency" then
		state.number = tonumber(cur_token_str)
		state.expect = Token.IDENTIFIER
		state.expect_field = "trust"
		state.required = state.required - 1
		-- create true message id from frequency and message number
		local freq = state.frequency
		if freq == "High" then
			-- High is already correct.
			state.id = state.number
			state.id_length = 1
		elseif freq == "Medium" then
			state.id = tonumber("0xFF" .. string.format("%02X", state.number))
			state.id_length = 2
		elseif freq == "Low" then
			state.id = tonumber("0xFFFF" .. string.format("%04X", state.number))
			state.id_length = 4
		else
			-- Fixed is already correct.
			state.id = state.number
			state.id_length = 4
		end
	else
		error(string.format("unhandled message number: %s\n",cur_token_str))
	end
	return nil
end,
["{"]		= function(state)
	local block = run_parser(block_parser)
	table.insert(state,block)
	-- add min length of block to minimal length of message
	local min_length = block.min_length
	if block.has_count then
		-- add one byte for the block count
		min_length = min_length + 1
		state.fixed_length = false
	else
		-- if block is not fixed length then message can't be fixed length.
		if not block.fixed_length then
			state.fixed_length = false
		end
		min_length = min_length * block.count
	end
	state.min_length = state.min_length + min_length
end,
["}"]		= function(state)
	if state.required > 0 then
		error("missing " .. state.required .. " fields")
	end
	-- clean state.
	state.required = nil
	state.expect = nil
	state.expect_field = nil
	return state
end,
eof = function(state)
	error("missing '}' token at end of message: " .. state.name)
end,
}
 
--
-- Template file parser
--
local template_parser = {
name = "message_template",
unhandled_error = false,
init = function()
	return {
		version = 0,
		msg_count = 0,
		msgs = {}
	}
end,
[Token.IDENTIFIER] 	= function(state)
	-- handle version
	if cur_token_str == "version" then
		state.expect = Token.NUMBER
		state.last_ident = cur_token_str
	else
		error(string.format("unknown template identifier: %s\n",cur_token_str))
	end
	return nil
end,
[Token.NUMBER]			= function(state)
	-- handle version number
	if state.last_ident == "version" then
		state.last_ident = nil
		state.version = tonumber(cur_token_str)
		-- check version number
		if state.version ~= 2 then
			error("invalid verion: " .. state.version)
		end
	else
		error(string.format("unhandled template number: %s\n",cur_token_str))
	end
	return nil
end,
["{"]		= function(state)
	local message = run_parser(message_parser)
	state.msg_count = state.msg_count + 1
	state.msgs[message.id] = message
end,
["}"]		= function(state)
	error(string.format("unhandled '%s' token",cur_token_str))
end,
eof = function(state)
	return state
end,
}
 
function parse_template(file)
	-- create lexer
	local status, ret = pcall(get_lexer,file)
	if not status then
		ret = string.format("LLUDP: Failed parse file into tokens: %s\n%s\n", file, ret)
		error(ret, 0)
		return nil
	end
	lexer = ret
	-- parse template file
	local status, ret = pcall(run_parser,template_parser)
	if not status then
		ret = string.format("LLUDP: Failed parsing on line %s:%d: '%s'\n%s\n",
			file, lexer.get_line_number(), lexer.get_line(), ret)
		error(ret, 0)
		return nil
	end
	io.write("finished parsing: " .. file .. "\n")
	-- return list of messages parsed from file.
	return ret
end
 
--parse_template("message_template.msg")
--print_tokens("message_template.msg")

File "lexer.lua"

--[[
BSD-Licensed:
Copyright (c) 2008, Robert G. Jakabosky <bobby@sharedrealm.com>
All rights reserved.
 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 
    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the SharedRealm nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
]]
 
-- Token types.
Token = {
NONE         = -1,
IDENTIFIER   = -2,
NUMBER       = -3,
COMMENT      = -6,
EOL          = -7,
["{"] = "{",
["}"] = "}",
}
TokenNames = {
[-1] = "NONE",
[-2] = "IDENTIFIER",
[-3] = "NUMBER",
[-6] = "COMMENT",
[-7] = "EOL",
["{"] = "{",
["}"] = "}",
}
 
-- parse line into array of tokens.
local function default_parse_tokens(line)
	local tokens = {}
	local comment = nil
	-- check for a comment on this line.
	local idx = line:find("//")
	if idx ~= nil then
		comment = {Token.COMMENT, line:sub(idx)}
		-- remove comment from line
		line = line:sub(1,idx - 1)
	end
 
	-- split line into tokens using white-space as token delimitator
	for tok in line:gmatch("%s?([^%s]+)") do
		local tok_type = Token.NONE
		-- check for number
		if tonumber(tok) ~= nil then
			tok_type = Token.NUMBER
		elseif Token[tok] then
			-- token is same as type
			tok_type = Token[tok]
		else
			-- token is an identifier
			tok_type = Token.IDENTIFIER
		end
		table.insert(tokens,{tok_type,tok})
	end
	-- insert comment token.
	if comment ~= nil then
		table.insert(tokens,comment)
	end
	-- add token to mark the end of this line
	table.insert(tokens,{Token.EOL, ""})
	return tokens
end
 
function get_lexer(file, parse_tokens)
	-- use the default line tokenizer if one is not provided
	if parse_tokens == nil then parse_tokens = default_parse_tokens end
	-- next/current line code
	local line_num = 0
	local line = nil
	local next_line = io.lines(file)
	-- parse line tokens code
	local get_next_token = nil
	local next_tokens = function ()
		local f, tokens, idx
		repeat
			line_num = line_num + 1
			line = next_line()
			if line == nil then return nil end
			tokens = parse_tokens(line)
		until tokens ~= nil
		-- create get_next_toekn function from table iterator
		f, tokens, idx = ipairs(tokens)
		get_next_token = function()
			idx, token = f(tokens, idx)
			return token
		end
		return tokens
	end
 
	-- get first group of tokens
	if next_tokens() == nil then
		-- error reading file or empty file
		return nil
	end
	-- build lexer table.
	local lexer = {
	get_token = function ()
		local token
		repeat
			token = get_next_token()
			if token == nil then
				-- get next group of tokens
				if next_tokens() == nil then
					-- end of file.
					return nil
				end
			end
		until token ~= nil
		return token
	end,
	get_line_number = function() return line_num end,
	get_line = function() return line end
	}
	return lexer
end
 
function print_tokens(file)
	local lexer = get_lexer(file)
	local num = -1
	while true do
		local tok = lexer.get_token()
		if tok == nil then
			break
		end
		if num ~= lexer.get_line_number() then
			num = lexer.get_line_number()
			io.write("\n")
			io.write(string.format("%d: ",num))
		end
		io.write(string.format("%s ",tok[2]))
	end
	io.write("\n")
end
Personal tools
General
About This Wiki