File Coverage

blib/lib/Game/Tibia/Packet.pm
Criterion Covered Total %
statement 74 84 88.1
branch 24 42 57.1
condition 4 9 44.4
subroutine 12 13 92.3
pod 5 5 100.0
total 119 153 77.7


line stmt bran cond sub pod time code
1 7     7   107070 use strict;
  7         17  
  7         195  
2 7     7   36 use warnings;
  7         16  
  7         177  
3 7     7   74 use v5.16.0;
  7         23  
4             package Game::Tibia::Packet;
5              
6             # ABSTRACT: Minimal session layer support for the MMORPG Tibia
7             our $VERSION = '0.006'; # VERSION
8              
9 7     7   1099 use Digest::Adler32 qw(adler32);
  7         1361  
  7         218  
10 7     7   1728 use Crypt::XTEA 0.0108;
  7         15086  
  7         269  
11 7     7   2243 use Crypt::ECB 2.0.0;
  7         15659  
  7         328  
12 7     7   52 use Carp;
  7         17  
  7         4096  
13              
14             sub version;
15              
16             =pod
17              
18             =encoding utf8
19              
20             =head1 NAME
21              
22             Game::Tibia::Packet - Session layer support for the MMORPG Tibia
23              
24             =head1 SYNOPSIS
25              
26             use Game::Tibia::Packet;
27              
28             # decrypt Tibia packet
29             my $read; my $ret = $sock->recv($read, 1024);
30             my $res = Game::Tibia::Packet->new(packet => $read, xtea => $xtea_key);
31             $packet_type = unpack('C', $res->payload);
32              
33              
34             # encrypt a Tibia speech packet
35             my $p = Game::Tibia::Packet->new;
36             $p->payload .= pack("C S S S/A S C SSC S/A",
37             0xAA, 0x1, 0x0, "Perl", 0, 1, 1, 1, 8,
38             "Game::Tibia::Packet says Hi!\n:-)");
39             $sock->send($p->finalize($xtea_key}))
40              
41             =begin HTML
42              
43            

Screenshot

44              
45             =end HTML
46              
47              
48             =head1 DESCRIPTION
49              
50             Methods for constructing Tibia Gameserver (XTEA) packets. Handles checksum calculation and symmetric encryption depending on the requested Tibia version.
51              
52             Should work with all Tibia versions less than 9.80.
53              
54             =head1 METHODS AND ARGUMENTS
55              
56             =over 4
57              
58             =item new(version => $version, [packet => $payload, xtea => $xtea])
59              
60             Constructs a new Game::Tibia::Packet instance of version C<$version>. If payload and XTEA are given, the payload will be decrypted and trimmed to correct size.
61              
62             =cut
63              
64             sub new {
65 6     6 1 178 my $type = shift;
66 6         33 my $self = {
67             payload => '',
68             packet => '',
69             xtea => undef,
70             padding => '',
71             @_
72             };
73              
74 6 50 33     37 croak 'A protocol version < 9.80 must be supplied' if !defined $self->{version} || $self->{version} >= 980;
75 6 50       25 $self->{versions}{client} = version $self->{version} unless ref $self->{version};
76              
77 6 100       17 if ($self->{packet} ne '')
78             {
79             #return undef unless isValid($self->{packet});
80             my $ecb = Crypt::ECB->new(
81 3         58 -cipher => Crypt::XTEA->new($self->{xtea}, 32, little_endian => 1)
82             );
83 3         430 $ecb->padding('null');
84              
85 3 50       29 my $digest_size = defined $self->{versions}{client}{adler32} ? 4 : 0;
86 3         12 $self->{payload} = $ecb->decrypt(substr($self->{packet}, 2 + $digest_size));
87 3         1211 $self->{payload} .= "\0" x ((8 - length($self->{payload})% 8)%8);
88 3         14 $self->{padding} = substr $self->{payload}, 2 + unpack('v', $self->{payload});
89 3         27 $self->{payload} = substr $self->{payload}, 2, unpack('v', $self->{payload});
90             }
91              
92 6         12 bless $self, $type;
93 6         14 return $self;
94             }
95              
96             =item isValid($packet)
97              
98             Checks if packet's adler32 digest matches (A totally unnecessary thing on Cipsoft's part, as we already have TCP checksum. Why hash again?)
99              
100             =cut
101              
102             sub isValid {
103 0     0 1 0 my $packet = shift;
104              
105 0         0 my ($len, $adler) = unpack('(S a4)<', $packet);
106 0 0       0 return 0 if $len + 2 != length $packet;
107              
108 0         0 my $a32 = Digest::Adler32->new;
109 0         0 $a32->add(substr($packet, 6));
110 0 0       0 return 0 if $a32->digest ne reverse $adler;
111 0         0 1;
112             #TODO: set errno to checksum failed or length doesnt match
113             }
114              
115             =item payload() : lvalue
116              
117             returns the payload as lvalue (so you can concat on it)
118              
119             =cut
120              
121             sub payload : lvalue {
122 12     12 1 15 my $self = shift;
123 12         52 return $self->{payload};
124             }
125              
126             =item finalize([$XTEA_KEY])
127              
128             Finalizes the packet. XTEA encrypts, prepends checksum and length.
129              
130             =cut
131              
132              
133             sub finalize {
134 3     3 1 9 my $self = shift;
135 3   66     12 my $XTEA = $self->{xtea} // shift;
136              
137 3         8 my $packet = $self->{payload};
138 3 50 33     25 if ($self->{versions}{client}{xtea} and defined $XTEA) {
139 3         12 $packet = pack('v', length $packet) . $packet;
140              
141 3         12 my $ecb = Crypt::ECB->new(
142             -cipher => Crypt::XTEA->new($XTEA, 32, little_endian => 1)
143             );
144 3         267 $ecb->padding('null');
145              
146             # $packet .= "\0" x ((8 - length($packet)% 8)%8);
147 3         24 my $padding_len = (8 - length($packet)% 8)%8;
148 3         13 $packet .= pack("a$padding_len", unpack('a*', $self->{padding}));
149 3         6 my $orig_len = length $packet;
150 3         7 $packet = $ecb->encrypt($packet);
151 3         1439 substr($packet, $orig_len) = '';
152             }
153              
154 3         10 my $digest = '';
155 3 50       15 if ($self->{versions}{client}{adler32}) {
156 0         0 my $a32 = Digest::Adler32->new;
157 0         0 $a32->add($packet);
158 0         0 $digest = unpack 'H*', pack 'N', unpack 'L', $a32->digest;
159             }
160              
161 3         17 $packet = CORE::pack("S/a", $digest.$packet);
162              
163 3         25 $packet;
164             }
165              
166              
167             =item version($version)
168              
169             Returns a hash reference with protocol traits. For example for 840, it returns:
170              
171             { gmbyte => 1, outfit_addons => 1, adler32 => 1, acc_name => 1,
172             stamina => 1, xtea => 1, VERSION => 840, rsa => 1, lvl_on_msg => 1 };
173              
174             =cut
175              
176 7     7   57 use constant TRUE => 1;
  7         17  
  7         2222  
177              
178             sub version {
179 12     12 1 24 my $version = shift;
180 12 50       32 $version = $version->{VERSION} if ref $version;
181 12         72 $version =~ s/^v|[ .]//g;
182 12 50       50 $version =~ /^\d+/ or croak 'Version format invalid';
183              
184 12         22 my %has;
185              
186 12         29 $has{gmbyte} = 1; # Not sure when the GM byte first appeared
187              
188             ($version >= 761) # 761 was a test client. 770 was the first release
189 12 100       42 and $has{xtea} = $has{rsa} = TRUE;
190             ($version >= 780)
191 12 100       49 and $has{outfit_addons} = $has{stamina} = $has{lvl_on_msg} = TRUE;
192             ($version >= 830)
193 12 100       34 and $has{adler32} = $has{acc_name} = TRUE;
194             ($version >= 841)
195 12 100       35 and $has{hwinfo} = $has{nonce} = TRUE;
196             ($version >= 953)
197 12 50       26 and $has{ping} = TRUE;
198             ($version >= 980)
199 12 50       28 and $has{client_version} = $has{game_preview} = TRUE;
200             ($version >= 1010)
201 12 50       27 and $has{worldlist_in_charlist} = TRUE;
202             ($version >= 1061)
203 12 50       22 and $has{extra_gpu_info} = TRUE;
204             ($version >= 1071)
205 12 50       26 and $has{game_content_revision} = TRUE;
206             ($version >= 1072)
207 12 50       22 and $has{auth_token} = TRUE;
208             ($version >= 1074)
209 12 50       27 and $has{session_key} = TRUE;
210              
211 12         22 $has{VERSION} = $version;
212              
213 12         37 return \%has;
214             }
215              
216             1;
217             __END__