|
|
(18 intermediate revisions by 6 users not shown) |
Line 1: |
Line 1: |
| + | {{Quicklinks|LLUDP_Dissector}} |
| + | |
| == LLUDP protocol dissector == | | == 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.
| + | This page describes the Wireshark LLUDP Protocol Dissector that can parse the message_template.msg file and use that information to decode all the message fields from the Linden Lab UDP protocol. Up to date code is available on [https://github.com/Neopallium/lludp_dissector GitHub] |
| | | |
| == Installing == | | == Installing == |
− | * Requires wireshark with Lua 5.1.x support. [http://wiki.wireshark.org/Lua See this page for getting wireshark to support lua] | + | * Requires Wireshark with Lua support. This is enabled by default in recent versions of Wireshark. [http://wiki.wireshark.org/Lua See this page for Wireshark support for Lua] |
| | | |
| === on Linux === | | === on Linux === |
− | * Copy all four source files into ~/.wireshark | + | * '''Check if correct''' - Copy all five source files into ~/.wireshark |
− | * 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. | + | * '''Check if correct''' - 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. | + | * '''Check if correct''' - 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 === | | === on Windows === |
− | * Copy all four source files into your user profiles directory | + | * Copy the lludp folder containing the five .lua source files into the Wireshark AppData directory |
| | | |
− | '''Vista*''' | + | '''Windows versions up to Windows 11''' |
| | | |
− | C:\Users\<username>\AppData\Roaming\Wireshark | + | C:\Users\<username>\AppData\Roaming\Wireshark\Plugins |
| | | |
| '''XP/2000''' | | '''XP/2000''' |
| | | |
− | C:\Documents and Settings\<username>\Application Data\Wireshark | + | C:\users\<username>\AppData\Wireshark |
| | | |
− | * Edit C:\Program Files\Wireshark\init.lua and change ''disable_lua'' to ''false'' (default is true) | + | * init.lua is not required as LUA support is enabled by default in recent versions of Wireshark. |
− | | + | |
− | * *Note: I have only tested this on Windows XP
| + | |
| | | |
| == LLUDP preferences == | | == LLUDP preferences == |
− | There are three preferences that can be changed from wiresharks "Preferences" dialog: | + | There are three preferences that can be changed from Wireshark's Preferences - Protocols - LLUDP dialog: |
− | * Message template file: Full path to the message_template.msg file used to decode message name & details from the packets. | + | * Message template file: Full path to the message_template.msg file used to decode message name & details from the packets. On Windows use double backslash '\\' instead of single blackslash '\' to separate directories (Example "C:\\Program Files\\FirestormOS-Releasex64\\app_settings\\message_template.msg"). |
| * UDP port range start: First UDP port to mark as LLUDP packets. (default 13000) | | * 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) | | * 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. | + | If your OpenSimulator regions are using only ports 9000-9050 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" ===
| + | |
− | <source lang="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")
| + | |
− | | + | |
− | </source>
| + | |
− | | + | |
− | === File "lludp.lua" ===
| + | |
− | <source lang="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_lluid = ProtoField.new("LLUID", "lludp.var.lluid", "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_lluid, 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)
| + | |
− | </source>
| + | |
− | | + | |
− | === File "llmessage.lua" ===
| + | |
− | <source lang="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")
| + | |
− | | + | |
− | </source>
| + | |
− | | + | |
− | === File "lexer.lua" ===
| + | |
− | <source lang="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
| + | |
| | | |
− | </source>
| + | === Code license === |
| + | The Wireshark LLUDP Dissector maybe used under the terms of the "Simplified BSD License" or the [https://github.com/Neopallium/lludp_dissector/blob/master/LICENSE_GPL.txt GPL]. |
This page describes the Wireshark LLUDP Protocol Dissector that can parse the message_template.msg file and use that information to decode all the message fields from the Linden Lab UDP protocol. Up to date code is available on GitHub
There are three preferences that can be changed from Wireshark's Preferences - Protocols - LLUDP dialog:
If your OpenSimulator regions are using only ports 9000-9050 then change the UDP port range.
The Wireshark LLUDP Dissector maybe used under the terms of the "Simplified BSD License" or the GPL.