-- PokerTH Wireshark Lua dissector (extended for Announce and Init messages) local p_pokerth = Proto("pokerth", "PokerTH Protocol") -- Port to dissect local tcp_port = 7234 -- Message type map local message_type_names = { [1] = "AnnounceMessage", [2] = "InitMessage", [3] = "AuthServerChallengeMessage", [4] = "AuthClientResponseMessage", [5] = "AuthServerVerificationMessage", [6] = "InitAckMessage", [7] = "AvatarRequestMessage", [8] = "AvatarHeaderMessage", [9] = "AvatarDataMessage", [10] = "AvatarEndMessage", [11] = "UnknownAvatarMessage", [12] = "PlayerListMessage", [13] = "GameListNewMessage", [14] = "GameListUpdateMessage", [15] = "GameListPlayerJoinedMessage", [16] = "GameListPlayerLeftMessage", [17] = "GameListAdminChangedMessage", [18] = "PlayerInfoRequestMessage", [19] = "PlayerInfoReplyMessage", [20] = "SubscriptionRequestMessage", [21] = "JoinExistingGameMessage", [22] = "JoinNewGameMessage", [23] = "RejoinExistingGameMessage", [24] = "JoinGameAckMessage", [25] = "JoinGameFailedMessage", [26] = "GamePlayerJoinedMessage", [27] = "GamePlayerLeftMessage", [28] = "GameAdminChangedMessage", [29] = "RemovedFromGameMessage", [30] = "KickPlayerRequestMessage", [31] = "LeaveGameRequestMessage", [32] = "InvitePlayerToGameMessage", [33] = "InviteNotifyMessage", [34] = "RejectGameInvitationMessage", [35] = "RejectInvNotifyMessage", [36] = "StartEventMessage", [37] = "StartEventAckMessage", [38] = "GameStartInitialMessage", [39] = "GameStartRejoinMessage", [40] = "HandStartMessage", [41] = "PlayersTurnMessage", [42] = "MyActionRequestMessage", [43] = "YourActionRejectedMessage", [44] = "PlayersActionDoneMessage", [45] = "DealFlopCardsMessage", [46] = "DealTurnCardMessage", [47] = "DealRiverCardMessage", [48] = "AllInShowCardsMessage", [49] = "EndOfHandShowCardsMessage", [50] = "EndOfHandHideCardsMessage", [51] = "ShowMyCardsRequestMessage", [52] = "AfterHandShowCardsMessage", [53] = "EndOfGameMessage", [54] = "PlayerIdChangedMessage", [55] = "AskKickPlayerMessage", [56] = "AskKickDeniedMessage", [57] = "StartKickPetitionMessage", [58] = "VoteKickRequestMessage", [59] = "VoteKickReplyMessage", [60] = "KickPetitionUpdateMessage", [61] = "EndKickPetitionMessage", [62] = "StatisticsMessage", [63] = "ChatRequestMessage", [64] = "ChatMessage", [65] = "ChatRejectMessage", [66] = "DialogMessage", [67] = "TimeoutWarningMessage", [68] = "ResetTimeoutMessage", [69] = "ReportAvatarMessage", [70] = "ReportAvatarAckMessage", [71] = "ReportGameMessage", [72] = "ReportGameAckMessage", [73] = "ErrorMessage", [74] = "AdminRemoveGameMessage", [75] = "AdminRemoveGameAckMessage", [76] = "AdminBanPlayerMessage", [77] = "AdminBanPlayerAckMessage", [78] = "GameListSpectatorJoinedMessage", [79] = "GameListSpectatorLeftMessage", [80] = "GameSpectatorJoinedMessage", [81] = "GameSpectatorLeftMessage" } -- Enum mappings local server_types = { [0] = "LAN", [1] = "InternetNoAuth", [2] = "InternetAuth" } local login_types = { [0] = "Guest", [1] = "Authenticated", [2] = "Unauthenticated" } -- Fields local f_length = ProtoField.uint32("pokerth.length", "Packet Length", base.DEC) local f_type = ProtoField.uint8("pokerth.type", "Message Type", base.DEC, message_type_names) -- Version fields (used inside AnnounceMessage) f_version_major = ProtoField.uint32("pokerth.version.major", "Major Version", base.DEC) f_version_minor = ProtoField.uint32("pokerth.version.minor", "Minor Version", base.DEC) -- AnnounceMessage fields f_announce_beta_rev = ProtoField.uint32("pokerth.announce.beta_revision", "Latest Beta Revision", base.DEC) f_announce_server_type = ProtoField.string("pokerth.announce.server_type", "Server Type") f_announce_num_players = ProtoField.uint32("pokerth.announce.num_players", "Players On Server", base.DEC) server_type_enum = { [0] = "LAN", [1] = "Internet (No Auth)", [2] = "Internet (Auth)" } local f_build_id = ProtoField.uint32("pokerth.build_id", "Build ID", base.DEC) local f_login_type = ProtoField.uint32("pokerth.login", "Login Type", base.DEC, login_types) -- Define InitMessage subtree fields local f_init_requested_version = ProtoField.string("pokerth.init.requested_version", "Requested Version") local f_init_build_id = ProtoField.uint32("pokerth.init.build_id", "Build ID", base.DEC) local f_init_last_session = ProtoField.bytes("pokerth.init.last_session", "Last Session ID") local f_init_password = ProtoField.string("pokerth.init.password", "Auth Server Password") local f_init_login = ProtoField.uint8("pokerth.init.login", "Login Type", base.DEC, { [0] = "Guest Login", [1] = "Authenticated Login", [2] = "Unauthenticated Login" }) local f_init_nickname = ProtoField.string("pokerth.init.nickname", "Nickname") local f_init_userdata = ProtoField.bytes("pokerth.init.client_userdata", "Client User Data") local f_init_avatarhash = ProtoField.bytes("pokerth.init.avatarhash", "Avatar Hash") f_initack_yourSessionId = ProtoField.bytes("pokerth.initack_yourSessionId", "Your Session ID") f_initack_yourPlayerId = ProtoField.uint32("pokerth.initack_yourPlayerId", "Your Player ID", base.DEC) f_initack_yourAvatarHash = ProtoField.bytes("pokerth.initack_yourAvatarHash", "Your Avatar Hash") f_initack_rejoinGameId = ProtoField.uint32("pokerth.initack_rejoinGameId", "Rejoin Game ID", base.DEC) f_playerlist_id = ProtoField.uint32("pokerth.playerlist.id", "Player ID", base.DEC) f_playerlist_notify = ProtoField.uint8("pokerth.playerlist.notification", "Player List Notification", base.DEC, { [0] = "playerListNew", [1] = "playerListLeft" }) f_gamelist_game_id = ProtoField.uint32("pokerth.gamelist.game_id", "Game ID", base.DEC) f_gamelist_game_mode = ProtoField.uint8("pokerth.gamelist.game_mode", "Game Mode", base.DEC) f_gamelist_private = ProtoField.bool("pokerth.gamelist.private", "Is Private") f_gamelist_players = ProtoField.bytes("pokerth.gamelist.players", "Player IDs") f_gamelist_players_id = ProtoField.uint32("pokerth.gamelist.player_id", "Player ID", base.DEC) f_gamelist_admin_id = ProtoField.uint32("pokerth.gamelist.admin_id", "Admin Player ID", base.DEC) f_gamelist_game_info = ProtoField.bytes("pokerth.gamelist.game_info", "NetGameInfo") f_gamelist_spectators = ProtoField.bytes("pokerth.gamelist.spectators", "Spectator IDs") f_gamelist_spectator_id = ProtoField.uint32("pokerth.gamelist.spectator_id", "Spectator ID", base.DEC) -- NetGameInfo fields local f_gameinfo_name = ProtoField.string("pokerth.gameinfo.name", "Game Name") local f_gameinfo_type = ProtoField.uint32("pokerth.gameinfo.type", "Net Game Type", base.DEC, { [1] = "Normal", [2] = "Registered Only", [3] = "Invite Only", [4] = "Ranking" }) local f_gameinfo_max_players = ProtoField.uint32("pokerth.gameinfo.max_players", "Max Number of Players") local f_gameinfo_raise_mode = ProtoField.uint32("pokerth.gameinfo.raise_mode", "Raise Interval Mode", base.DEC, { [1] = "On Hand Num", [2] = "On Minutes" }) local f_gameinfo_raise_hands = ProtoField.uint32("pokerth.gameinfo.raise_hands", "Raise Every Hands") local f_gameinfo_raise_minutes = ProtoField.uint32("pokerth.gameinfo.raise_minutes", "Raise Every Minutes") local f_gameinfo_end_mode = ProtoField.uint32("pokerth.gameinfo.end_mode", "End Raise Mode", base.DEC, { [1] = "Double Blinds", [2] = "Raise By End Value", [3] = "Keep Last Blind" }) local f_gameinfo_end_blind = ProtoField.uint32("pokerth.gameinfo.end_blind", "End Raise Small Blind Value") local f_gameinfo_gui_speed = ProtoField.uint32("pokerth.gameinfo.gui_speed", "Proposed GUI Speed") local f_gameinfo_delay = ProtoField.uint32("pokerth.gameinfo.delay", "Delay Between Hands (s)") local f_gameinfo_action_timeout = ProtoField.uint32("pokerth.gameinfo.action_timeout", "Player Action Timeout (s)") local f_gameinfo_first_blind = ProtoField.uint32("pokerth.gameinfo.first_blind", "First Small Blind") local f_gameinfo_start_money = ProtoField.uint32("pokerth.gameinfo.start_money", "Start Money") local f_gameinfo_manual_blinds = ProtoField.uint32("pokerth.gameinfo.manual_blinds", "Manual Blinds", base.DEC) local f_gameinfo_allow_spectators = ProtoField.bool("pokerth.gameinfo.allow_spectators", "Allow Spectators") f_player_info_id = ProtoField.uint32("pokerth.playerinfo.player_id", "Player ID", base.DEC) f_playerinfo_reply_id = ProtoField.uint32("pokerth.playerinfo.id", "Player ID", base.DEC) f_playerinfo_name = ProtoField.string("pokerth.playerinfo.name", "Player Name") f_playerinfo_ishuman = ProtoField.bool("pokerth.playerinfo.ishuman", "Is Human") f_playerinfo_rights = ProtoField.uint8("pokerth.playerinfo.rights", "Player Rights", base.DEC) f_playerinfo_avatar_type = ProtoField.uint8("pokerth.playerinfo.avatar.type", "Avatar Type", base.DEC) f_playerinfo_avatar_hash = ProtoField.bytes("pokerth.playerinfo.avatar.hash", "Avatar Hash") f_chat_game_id = ProtoField.uint32("pokerth.chat.gameid", "Game ID", base.DEC) f_chat_player_id = ProtoField.uint32("pokerth.chat.playerid", "Player ID", base.DEC) f_chat_type = ProtoField.uint8("pokerth.chat.type", "Chat Type", base.DEC, { [0] = "Lobby", [1] = "Game", [2] = "Bot", [3] = "Broadcast", [4] = "Private" }) f_chat_text = ProtoField.string("pokerth.chat.text", "Chat Text") f_spectator_game_id = ProtoField.uint32("pokerth.spectator.game_id", "Spectator Game ID", base.DEC) f_spectator_player_id = ProtoField.uint32("pokerth.spectator.player_id", "Spectator Player ID", base.DEC) f_player_joined_gameid = ProtoField.uint32("pokerth.playerjoined.gameid", "Game ID", base.DEC) f_player_joined_playerid = ProtoField.uint32("pokerth.playerjoined.playerid", "Player ID", base.DEC) f_player_left_gameid = ProtoField.uint32("pokerth.playerleft.gameid", "Game ID", base.DEC) f_player_left_playerid = ProtoField.uint32("pokerth.playerleft.playerid", "Player ID", base.DEC) p_pokerth.fields = { -- Common header fields f_length, f_type, -- AnnounceMessage fields f_version_major, f_version_minor, f_announce_beta_rev, f_announce_server_type, f_announce_num_players, -- InitMessage fields f_message_type, f_init_requested_version, f_init_build_id, f_init_last_session, f_init_password, f_init_login, f_init_nickname, f_init_userdata, f_init_avatarhash, f_initack_yourSessionId, f_initack_yourPlayerId, f_initack_yourAvatarHash, f_initack_rejoinGameId, f_playerlist_id, f_playerlist_notify, f_gamelist_game_id, f_gamelist_game_mode, f_gamelist_private, f_gamelist_players, f_gamelist_players_id, f_gamelist_admin_id, f_gamelist_game_info, f_gamelist_spectators, f_gamelist_spectator_id, f_gameinfo_name, f_gameinfo_type, f_gameinfo_max_players, f_gameinfo_raise_mode, f_gameinfo_raise_hands, f_gameinfo_raise_minutes, f_gameinfo_end_mode, f_gameinfo_end_blind, f_gameinfo_gui_speed, f_gameinfo_delay, f_gameinfo_action_timeout, f_gameinfo_first_blind, f_gameinfo_start_money, f_gameinfo_manual_blinds, f_gameinfo_allow_spectators, f_player_info_id, f_playerinfo_reply_id, f_playerinfo_name, f_playerinfo_ishuman, f_playerinfo_rights, f_playerinfo_avatar_type, f_playerinfo_avatar_hash, f_chat_game_id, f_chat_player_id, f_chat_type, f_chat_text, f_spectator_game_id, f_spectator_player_id, f_player_joined_gameid, f_player_joined_playerid, f_player_left_gameid, f_player_left_playerid, } -- Dissector function function read_varint(tvb, offset) local result = 0 local shift = 0 local consumed = 0 local max_offset = tvb:len() while offset + consumed < max_offset and consumed < 10 do local b = tvb(offset + consumed, 1):uint() result = result + bit.lshift(bit.band(b, 0x7F), shift) shift = shift + 7 consumed = consumed + 1 if bit.band(b, 0x80) == 0 then break end end return result, consumed end -- Utility to decode the next protobuf field tag and length (if applicable) function read_protobuf_field_header(tvb, offset) local start_offset = offset -- Read tag (varint encoded) local tag, tag_len = read_varint(tvb, offset) offset = offset + tag_len local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) local length = nil local length_len = 0 -- Handle length-delimited (wire type 2) if wire_type == 2 then length, length_len = read_varint(tvb, offset) offset = offset + length_len end return { field_number = field_number, wire_type = wire_type, tag_len = tag_len, length = length, length_len = length_len, total_len = offset - start_offset, next_offset = offset } end function parse_net_game_info(tvb, tree) local offset = 0 while offset < tvb:len() do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 2 then -- string gameName local len, len_size = read_varint(tvb, offset) offset = offset + len_size tree:add(f_gameinfo_name, tvb(offset, len)) offset = offset + len elseif field_number == 2 and wire_type == 0 then -- enum netGameType local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_type, tvb(offset, size)):append_text(" (" .. tostring(value) .. ")") offset = offset + size elseif field_number == 3 and wire_type == 0 then -- uint32 maxNumPlayers local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_max_players, tvb(offset, size)) offset = offset + size elseif field_number == 4 and wire_type == 0 then -- enum raiseIntervalMode local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_raise_mode, tvb(offset, size)):append_text(" (" .. tostring(value) .. ")") offset = offset + size elseif field_number == 5 and wire_type == 0 then -- optional raiseEveryHands local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_raise_hands, tvb(offset, size)) offset = offset + size elseif field_number == 6 and wire_type == 0 then -- optional raiseEveryMinutes local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_raise_minutes, tvb(offset, size)) offset = offset + size elseif field_number == 7 and wire_type == 0 then -- enum endRaiseMode local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_end_mode, tvb(offset, size)):append_text(" (" .. tostring(value) .. ")") offset = offset + size elseif field_number == 8 and wire_type == 0 then -- optional endRaiseSmallBlindValue local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_end_blind, tvb(offset, size)) offset = offset + size elseif field_number == 9 and wire_type == 0 then -- uint32 proposedGuiSpeed local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_gui_speed, tvb(offset, size)) offset = offset + size elseif field_number == 10 and wire_type == 0 then -- uint32 delayBetweenHands local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_delay, tvb(offset, size)) offset = offset + size elseif field_number == 11 and wire_type == 0 then -- uint32 playerActionTimeout local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_action_timeout, tvb(offset, size)) offset = offset + size elseif field_number == 12 and wire_type == 0 then -- uint32 firstSmallBlind local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_first_blind, tvb(offset, size)) offset = offset + size elseif field_number == 13 and wire_type == 0 then -- uint32 startMoney local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_start_money, tvb(offset, size)) offset = offset + size elseif field_number == 14 and wire_type == 2 then -- repeated uint32 manualBlinds [packed = true] local len, len_size = read_varint(tvb, offset) offset = offset + len_size local end_offset = offset + len while offset < end_offset do local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_manual_blinds, tvb(offset, size)):append_text(" (" .. tostring(value) .. ")") offset = offset + size end elseif field_number == 15 and wire_type == 0 then -- optional bool allowSpectators local value, size = read_varint(tvb, offset) tree:add(f_gameinfo_allow_spectators, tvb(offset, size)) offset = offset + size else -- Unknown field, skip or show expert info tree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unknown NetGameInfo field %d (wire type %d)", field_number, wire_type)) break end end end function server_type_to_string(val) if val == 0 then return "LAN" elseif val == 1 then return "Internet (No Auth)" elseif val == 2 then return "Internet (Auth)" else return "Unknown" end end function parse_version(tvb, tree) local offset = 0 while offset < tvb:len() do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) --tree:add(tvb(offset, 1), string.format("Version Tag: field=%d, wire=%d", field_number, wire_type)) offset = offset + 1 if wire_type ~= 0 then tree:add_expert_info(PI_MALFORMED, PI_ERROR, string.format("Unexpected wire type %d in version (field %d)", wire_type, field_number)) break end local val, len = read_varint(tvb, offset) if field_number == 1 then tree:add(f_version_major, tvb(offset, len), val) elseif field_number == 2 then tree:add(f_version_minor, tvb(offset, len), val) else tree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unknown field %d in Version message", field_number)) end offset = offset + len end end -- Parse the full announce message (after message type and length have been read) function parse_announce_message(tvb, tree) local offset = 0 local announce_end = tvb:len() local subtree = tree:add(tvb(), "AnnounceMessage") while offset < announce_end do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 2 then -- protocolVersion local len, len_len = read_varint(tvb, offset) offset = offset + len_len local version_tvb = tvb(offset, len) local version_tree = subtree:add(version_tvb, "Protocol Version") parse_version(version_tvb, version_tree) offset = offset + len elseif field_number == 2 and wire_type == 2 then -- latestGameVersion local len, len_len = read_varint(tvb, offset) offset = offset + len_len local version_tvb = tvb(offset, len) local version_tree = subtree:add(version_tvb, "Latest Game Version") parse_version(version_tvb, version_tree) offset = offset + len elseif field_number == 3 and wire_type == 0 then -- latestBetaRevision local val, len = read_varint(tvb, offset) subtree:add(f_announce_beta_rev, tvb(offset, len), val) offset = offset + len elseif field_number == 4 and wire_type == 0 then -- serverType local val, len = read_varint(tvb, offset) subtree:add(f_announce_server_type, tvb(offset, len), server_type_enum[val] or "Unknown (" .. val .. ")") offset = offset + len elseif field_number == 5 and wire_type == 0 then -- numPlayersOnServer local val, len = read_varint(tvb, offset) subtree:add(f_announce_num_players, tvb(offset, len), val) offset = offset + len else subtree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unhandled field %d with wire type %d tag %x offset %d", field_number, wire_type, tag, offset)) break end end end function parse_init_message(tvb, tree) local offset = 0 local subtree = tree:add(tvb(), "InitMessage") while offset < tvb:len() do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) tree:add(tvb(offset, 1), string.format("Init Tag: field=%d, wire=%d", field_number, wire_type)) offset = offset + 1 if field_number == 1 and wire_type == 2 then -- requested_version local len, len_len = read_varint(tvb, offset) offset = offset + len_len local sub_tvb = tvb(offset, len) local version_tree = tree:add(f_init_requested_version, sub_tvb) parse_version(sub_tvb, version_tree) offset = offset + len elseif field_number == 2 and wire_type == 0 then -- build_id local val, len = read_varint(tvb, offset) tree:add(f_init_build_id, tvb(offset, len), val) offset = offset + len elseif field_number == 3 and wire_type == 2 then -- last_session_id local len, len_len = read_varint(tvb, offset) offset = offset + len_len tree:add(f_init_last_session, tvb(offset, len)) offset = offset + len elseif field_number == 4 and wire_type == 2 then -- password (not present in your example) local len, len_len = read_varint(tvb, offset) offset = offset + len_len tree:add(f_init_password, tvb(offset, len)) offset = offset + len elseif field_number == 5 and wire_type == 0 then -- login_type local val, len = read_varint(tvb, offset) tree:add(f_init_login, tvb(offset, len), val) offset = offset + len elseif field_number == 6 and wire_type == 2 then -- nickname local len, len_len = read_varint(tvb, offset) offset = offset + len_len tree:add(f_init_nickname, tvb(offset, len), tvb(offset, len):string()) offset = offset + len elseif field_number == 7 and wire_type == 2 then -- userdata (not in this msg) local len, len_len = read_varint(tvb, offset) offset = offset + len_len tree:add(f_init_userdata, tvb(offset, len)) offset = offset + len elseif field_number == 8 and wire_type == 2 then -- avatarHash (not in this msg) local len, len_len = read_varint(tvb, offset) offset = offset + len_len tree:add(f_init_avatarhash, tvb(offset, len)) offset = offset + len else tree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unknown field %d (wire type %d)", field_number, wire_type)) break end end end function parse_init_ack_message(tvb, tree) local offset = 0 local subtree = tree:add(p_pokerth, tvb, "InitAckMessage") local pos = 0 while pos < tvb:len() do local tag = tvb(pos, 1):uint() local field = bit.rshift(tag, 3) local wire = bit.band(tag, 0x07) pos = pos + 1 if field == 1 and wire == 2 then local len, len_len = read_varint(tvb, pos) pos = pos + len_len subtree:add(f_initack_yourSessionId, tvb(pos, len)) pos = pos + len elseif field == 2 and wire == 0 then local val, vlen = read_varint(tvb, pos) subtree:add(f_initack_yourPlayerId, tvb(pos, vlen), val) pos = pos + vlen elseif field == 3 and wire == 2 then local len, len_len = read_varint(tvb, pos) pos = pos + len_len subtree:add(f_initack_yourAvatarHash, tvb(pos, len)) pos = pos + len elseif field == 4 and wire == 0 then local val, vlen = read_varint(tvb, pos) subtree:add(f_initack_rejoinGameId, tvb(pos, vlen), val) pos = pos + vlen else subtree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unknown field %d (wire type %d)", field, wire)) break end end end function parse_player_list_message(tvb, tree) local offset = 0 while offset < tvb:len() do local tag_byte = tvb(offset,1):uint() local field_number = bit.rshift(tag_byte, 3) local wire_type = bit.band(tag_byte, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 0 then -- playerId local value, size = read_varint(tvb, offset) tree:add(f_playerlist_id, tvb(offset, size), value) offset = offset + size elseif field == 13 and wire == 2 then -- playerId local len, len_len = read_varint(tvb, pos) pos = pos + len_len tree:add(f_playerlist_id, tvb(pos, len)) pos = pos + len elseif field_number == 2 and wire_type == 0 then -- playerListNotification local value, size = read_varint(tvb, offset) tree:add(f_playerlist_notify, tvb(offset, size), value) offset = offset + size else tree:add_expert_info(PI_MALFORMED, PI_WARN, string.format("Unknown %x field %d (wire type %d)", tag_byte, field_number, wire_type)) break end end end function parse_game_list_new_message(tvb, tree) local offset = 0 while offset < tvb:len() do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 0 then -- gameId local value, size = read_varint(tvb, offset) tree:add(f_gamelist_game_id, tvb(offset, size)) offset = offset + size elseif field_number == 2 and wire_type == 0 then -- gameMode (enum) local value, size = read_varint(tvb, offset) tree:add(f_gamelist_game_mode, tvb(offset, size)):append_text(" (" .. tostring(value) .. ")") offset = offset + size elseif field_number == 3 and wire_type == 0 then -- isPrivate (bool) local value, size = read_varint(tvb, offset) tree:add(f_gamelist_private, tvb(offset, size)) offset = offset + size elseif field_number == 4 and wire_type == 2 then -- repeated uint32 playerIds [packed] local len, len_size = read_varint(tvb, offset) offset = offset + len_size local end_offset = offset + len while offset < end_offset do local value, size = read_varint(tvb, offset) tree:add(f_gamelist_players_id, tvb(offset, size)):append_text(" (" .. tostring(value) .. ")") offset = offset + size end elseif field_number == 5 and wire_type == 0 then -- adminPlayerId local value, size = read_varint(tvb, offset) tree:add(f_gamelist_admin_id, tvb(offset, size)) offset = offset + size elseif field_number == 6 and wire_type == 2 then -- NetGameInfo local len, len_size = read_varint(tvb, offset) offset = offset + len_size local netgameinfo_tree = tree:add(f_gamelist_game_info, tvb(offset, len), "NetGameInfo") parse_net_game_info(tvb(offset, len):tvb(), netgameinfo_tree) offset = offset + len elseif field_number == 7 and wire_type == 2 then -- repeated uint32 spectatorIds [packed] local len, len_size = read_varint(tvb, offset) offset = offset + len_size local end_offset = offset + len while offset < end_offset do local value, size = read_varint(tvb, offset) tree:add(f_gamelist_spectator_id, tvb(offset, size)):append_text(" (" .. tostring(value) .. ")") offset = offset + size end else tree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unknown GameListNewMessage field %d (wire type %d)", field_number, wire_type)) break end end end function parse_player_info_request_message(tvb, tree) local offset = 0 local tvb_len = tvb:len() while offset < tvb_len do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 2 then -- packed repeated uint32 local len, len_len = read_varint(tvb, offset) offset = offset + len_len local end_offset = offset + len --local packed_tree = tree:add(tvb(offset, len), "Player IDs") tree:add(tvb(offset, len), "Player IDs") while offset < end_offset do local value, size = read_varint(tvb, offset) --packed_tree:add(f_player_info_id, tvb(offset, size), value) tree:add(f_player_info_id, tvb(offset, size), value) offset = offset + size end else tree:add_expert_info(PI_MALFORMED, PI_ERROR, string.format("Unexpected field %d with wire type %d tag=%x offset=%d", field_number, wire_type, tag, offset)) break end end end function parse_player_info_reply_message(tvb, tree) local offset = 0 local msg_len = tvb:len() local subtree = tree:add(tvb(), "PlayerInfoReplyMessage") while offset < msg_len do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 --subtree:add_expert_info(PI_INFO, PI_COMMENT, string.format("tag %x offset %d field %d wire %d", tag, offset, field_number, wire_type)) if field_number == 1 and wire_type == 0 then local value, size = read_varint(tvb, offset) subtree:add(f_playerinfo_reply_id, tvb(offset, size), value) offset = offset + size elseif field_number == 2 and wire_type == 2 then local len, len_len = read_varint(tvb, offset) offset = offset + len_len local info_tvb = tvb(offset, len) local info_tree = subtree:add(info_tvb, "PlayerInfoData") local ioff = 0 while ioff < info_tvb:len() do local tag = info_tvb(ioff, 1):uint() local fn = bit.rshift(tag, 3) local wt = bit.band(tag, 0x07) ioff = ioff + 1 if fn == 1 and wt == 2 then local slen, slen_len = read_varint(info_tvb, ioff) ioff = ioff + slen_len info_tree:add(f_playerinfo_name, info_tvb(ioff, slen)) ioff = ioff + slen elseif fn == 2 and wt == 0 then local val, size = read_varint(info_tvb, ioff) info_tree:add(f_playerinfo_ishuman, info_tvb(ioff, size), val) ioff = ioff + size elseif fn == 3 and wt == 0 then local val, size = read_varint(info_tvb, ioff) info_tree:add(f_playerinfo_rights, info_tvb(ioff, size), val) ioff = ioff + size elseif fn == 5 and wt == 2 then local alen, alen_len = read_varint(info_tvb, ioff) ioff = ioff + alen_len local avatar_tvb = info_tvb(ioff, alen) local avatar_tree = info_tree:add(avatar_tvb, "AvatarData") local aoff = 0 while aoff < avatar_tvb:len() do local atag = avatar_tvb(aoff, 1):uint() local afn = bit.rshift(atag, 3) local awt = bit.band(atag, 0x07) aoff = aoff + 1 if afn == 1 and awt == 0 then local val, size = read_varint(avatar_tvb, aoff) avatar_tree:add(f_playerinfo_avatar_type, avatar_tvb(aoff, size), val) aoff = aoff + size elseif afn == 2 and awt == 2 then local hlen, hlen_len = read_varint(avatar_tvb, aoff) aoff = aoff + hlen_len avatar_tree:add(f_playerinfo_avatar_hash, avatar_tvb(aoff, hlen)) aoff = aoff + hlen else break end end ioff = ioff + alen else break end end offset = offset + len else break end end end function parse_chat_message(tvb, tree) local offset = 0 while offset < tvb:len() do local tag_offset = offset local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 tree:add_expert_info(PI_INFO, PI_COMMENT, string.format("tag %x offset %d field %d wire %d", tag, offset, field_number, wire_type)) if field_number == 1 and wire_type == 0 then -- gameId local value, size = read_varint(tvb, offset) tree:add(f_chat_game_id, tvb(offset, size), value) offset = offset + size elseif field_number == 2 and wire_type == 0 then -- playerId local value, size = read_varint(tvb, offset) tree:add(f_chat_player_id, tvb(offset, size), value) offset = offset + size elseif field_number == 3 and wire_type == 0 then -- chatType local value, size = read_varint(tvb, offset) --offset = offset + size tree:add(f_chat_type, tvb(offset, size), value) offset = offset + size elseif field_number == 4 and wire_type == 2 then -- chatText local len, size = read_varint(tvb, offset) offset = offset + size tree:add(f_chat_text, tvb(offset, len), "Chat Text: " .. tvb(offset, len):string()) offset = offset + len else -- Unknown field tree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unknown field: %d (wire type %d) at offset %d", field_number, wire_type, tag_offset)) break end end end function parse_game_list_spectator_joined_message(tvb, tree) local offset = 0 while offset < tvb:len() do local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 0 then -- gameId local value, size = read_varint(tvb, offset) tree:add(f_spectator_game_id, tvb(offset, size), value) offset = offset + size elseif field_number == 2 and wire_type == 0 then -- playerId local value, size = read_varint(tvb, offset) tree:add(f_spectator_player_id, tvb(offset, size), value) offset = offset + size else tree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unhandled field: %d (wire type %d)", field_number, wire_type)) break end end end function parse_game_list_player_joined_message(tvb, tree) local offset = 0 -- Parse gameId (field 1) local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 0 then local game_id, varint_len = read_varint(tvb, offset) tree:add(f_player_joined_gameid, tvb(offset, varint_len), game_id) offset = offset + varint_len end -- Parse playerId (field 2) tag = tvb(offset, 1):uint() field_number = bit.rshift(tag, 3) wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 2 and wire_type == 0 then local player_id, varint_len = read_varint(tvb, offset) tree:add(f_player_joined_playerid, tvb(offset, varint_len), player_id) offset = offset + varint_len end end function parse_game_list_player_left_message(tvb, tree) local offset = 0 -- Parse gameId (field 1) local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 1 and wire_type == 0 then local game_id, varint_len = read_varint(tvb, offset) tree:add(f_player_left_gameid, tvb(offset, varint_len), game_id) offset = offset + varint_len end -- Parse playerId (field 2) tag = tvb(offset, 1):uint() field_number = bit.rshift(tag, 3) wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number == 2 and wire_type == 0 then local player_id, varint_len = read_varint(tvb, offset) tree:add(f_player_left_playerid, tvb(offset, varint_len), player_id) offset = offset + varint_len end end -- Dispatcher function table local MESSAGE_TYPE_ANNOUNCE = 1 local MESSAGE_TYPE_INIT = 2 local MESSAGE_TYPE_INIT_ACK = 6 local MESSAGE_TYPE_PLAYERLIST = 12 local MESSAGE_TYPE_GAME_LIST_NEW = 13 local MESSAGE_TYPE_PLAYER_JOINED = 15 local MESSAGE_TYPE_PLAYER_LEFT = 16 local MESSAGE_TYPE_PLAYER_INFO_REQUEST = 18 local MESSAGE_TYPE_PLAYER_INFO_REPLY = 19 local MESSAGE_TYPE_CHAT_MESSAGE = 64 local MESSAGE_TYPE_SPECTATOR_JOINED = 78 local message_parsers = { [MESSAGE_TYPE_ANNOUNCE] = parse_announce_message, [MESSAGE_TYPE_INIT] = parse_init_message, [MESSAGE_TYPE_INIT_ACK] = parse_init_ack_message, [MESSAGE_TYPE_PLAYERLIST] = parse_player_list_message, [MESSAGE_TYPE_GAME_LIST_NEW] = parse_game_list_new_message, [MESSAGE_TYPE_PLAYER_INFO_REQUEST] = parse_player_info_request_message, [MESSAGE_TYPE_PLAYER_INFO_REPLY] = parse_player_info_reply_message, [MESSAGE_TYPE_CHAT_MESSAGE] = parse_chat_message, [MESSAGE_TYPE_SPECTATOR_JOINED] = parse_game_list_spectator_joined_message, [MESSAGE_TYPE_PLAYER_JOINED] = parse_game_list_player_joined_message, [MESSAGE_TYPE_PLAYER_LEFT] = parse_game_list_player_left_message, } function p_pokerth.dissector(tvb, pinfo, tree) pinfo.cols.protocol = "PokerTH" local subtree = tree:add(p_pokerth, tvb(), "PokerTH Protocol") local offset = 0 local tvb_len = tvb:len() while offset + 4 <= tvb_len do local msg_len = tvb(offset, 4):uint() -- Check for bounds before slicing the message if offset + 4 + msg_len > tvb_len then -- Add expert info or bail out gracefully subtree:add_expert_info(PI_MALFORMED, PI_ERROR, "Truncated protobuf message") break end local msg_tvb = tvb(offset + 4, msg_len) --subtree:add(string.format("Msg @ %d len %d", offset, msg_len)) local msg_tree = subtree:add(p_pokerth, msg_tvb, "PokerTH Protobuf Message") parse_pokerth_message(msg_tvb, msg_tree) offset = offset + 4 + msg_len --msg_tree:add(string.format("Next @ %d len %d tvb_len=%d", offset, msg_len, tvb_len)) end end function parse_pokerth_message(tvb, tree) if tvb:len() == 0 then tree:add_expert_info(PI_MALFORMED, PI_ERROR, "Empty protobuf message") return end local offset = 0 local start_offset = offset -- Save this to highlight the full message -- Step 1: Read message type (field 1, varint) local tag = tvb(offset, 1):uint() local field_number = bit.rshift(tag, 3) local wire_type = bit.band(tag, 0x07) offset = offset + 1 if field_number ~= 1 or wire_type ~= 0 then tree:add_expert_info(PI_MALFORMED, PI_ERROR, string.format("Expected message type at field 1 (varint), got field=%d wire=%d", field_number, wire_type)) return end -- Step 2: Read the message type local msg_type, varint_len = read_varint(tvb, offset) offset = offset + varint_len local header = read_protobuf_field_header(tvb, offset) local msg_tree = tree:add(p_pokerth, tvb(start_offset), string.format("PokerTH MessageType %d field=%d wire=%d len=%d tag=%x len=%d", msg_type, header.field_number, header.wire_type, header.total_len, tag, header.length)) local parser = message_parsers[msg_type] if parser then if offset + header.length > tvb:len() then tree:add_expert_info(PI_MALFORMED, PI_ERROR, "Truncated inner message body") return end local tvb_msg = tvb(header.next_offset, header.length) parser(tvb_msg, msg_tree) else msg_tree:add_expert_info(PI_UNDECODED, PI_NOTE, string.format("Unknown or unhandled message type %d", msg_type)) end end -- Register the dissector local tcp_table = DissectorTable.get("tcp.port") tcp_table:add(tcp_port, p_pokerth)