File Coverage

blib/lib/Game/Tibia/Packet.pm
Criterion Covered Total %
statement 76 86 88.3
branch 26 44 59.0
condition 6 12 50.0
subroutine 13 14 92.8
pod 5 5 100.0
total 126 161 78.2


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

Screenshot

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