File Coverage

blib/lib/Net/OICQ.pm
Criterion Covered Total %
statement 45 555 8.1
branch 0 208 0.0
condition 0 33 0.0
subroutine 15 75 20.0
pod 0 58 0.0
total 60 929 6.4


line stmt bran cond sub pod time code
1             package Net::OICQ;
2              
3             # $Id: OICQ.pm,v 1.19 2007/06/16 12:35:08 tans Exp $
4              
5             # Copyright (c) 2002 - 2007 Shufeng Tan. All rights reserved.
6             #
7             # This package is free software and is provided "as is" without express
8             # or implied warranty. It may be used, redistributed and/or modified
9             # under the terms of the Perl Artistic License (see
10             # http://www.perl.com/perl/misc/Artistic.html)
11              
12 1     1   23282 use 5.008;
  1         5  
  1         122  
13 1     1   6 use strict;
  1         2  
  1         34  
14 1     1   6 use warnings;
  1         5  
  1         30  
15 1     1   1015 use bytes;
  1         9  
  1         4  
16 1     1   24 use Carp;
  1         3  
  1         70  
17 1     1   842 use FileHandle;
  1         12878  
  1         9  
18 1     1   1494 use IO::Socket::INET;
  1         1229866  
  1         12  
19 1     1   761 use Digest::MD5;
  1         2  
  1         52  
20 1     1   1331 use Encode;
  1         485220  
  1         152  
21              
22 1     1   765 use Crypt::OICQ qw(encrypt decrypt);
  1         4562  
  1         80  
23 1     1   561 use Net::OICQ::ClientEvent;
  1         3  
  1         82  
24              
25             our $VERSION = '1.6';
26              
27             #################### Begin OICQ protocol data ######################
28              
29             our $SERVER_DOMAIN = pack("H*", "74656e63656e742e636f6d"); # ;-)
30              
31             # An OICQ session may use UDP or TCP.
32              
33             # The first two bytes of a TCP packet are a short integer in network
34             # order (pack 'n'), which stores the data length including the leading
35             # two bytes. Other than these two bytes, the format of TCP packets is
36             # identical to that of UDP packets. The following description is
37             # for UDP packets only.
38              
39             # A QQ data segment always begins with ASCII STX and ends with ASCII ETX
40              
41 1     1   6 use constant STX => "\x02";
  1         3  
  1         68  
42 1     1   6 use constant ETX => "\x03";
  1         1  
  1         1971  
43              
44             # Bytes 0x01-0x02 seem to be client version
45              
46             # These two bytes used to be fixed at 0x01 0x00 for packets from servers
47             # but they may use the same value as client, as of July 2006
48              
49             # 0x06 0x2e for packets from GB client version 2000c build 630
50             # 0x07 0x2e for packets from En client version 2000c build 305
51             # 0x08 0x01 for packets from En client version 2000c build 630
52             # 0x09 0x09 for packets from GB client version 2000c build 1230b
53             # 0x0b 0x37 for packets from QQ 2003iii 0304
54             # 0x0e 0x2d for packets from GB client version 2005 sp1 V05.0.201.110
55             # 0x0f 0x5f for packets from GB client V06.0.200.410
56              
57             our $CLIENT_VER = "\x0f\x5f"; #"\x0e\x2d";
58              
59             # Bytes 0x03-0x04 indicate command
60              
61             our %CmdCode = (
62             logout => "\0\x01",
63             keep_alive => "\0\x02",
64             update_info => "\0\x04",
65             search_users => "\0\x05",
66             get_user_info => "\0\x06",
67             add_contact_1 => "\0\x09",
68             del_contact => "\0\x0a",
69             add_contact_2 => "\0\x0b",
70             set_mode => "\0\x0d",
71             ack_service_msg => "\0\x12",
72             send_msg => "\0\x16",
73             recv_msg => "\0\x17",
74             unknown_001a => "\0\x1a",
75             forbid_contact => "\0\x1c",
76             req_file_key => "\0\x1d", # provided by alexe
77             cell_phone_1 => "\0\x21", # provided by alexe
78             login => "\0\x22",
79             get_friends_list => "\0\x26",
80             get_online_friends => "\0\x27",
81             cell_phone_2 => "\0\x29", # provided by alexe
82             do_group => "\0\x30", # provided by alexe
83             #login_request => "\0\x62", # obsolete
84             recv_service_msg => "\0\x80",
85             recv_friend_status => "\0\x81",
86             login_request_1 => "\0\x91",
87             login_request_2 => "\0\xba",
88             );
89              
90             our %Cmd;
91             foreach my $cmd (keys %CmdCode) { $Cmd{$CmdCode{$cmd}} = $cmd }
92              
93             our %GrpCmdCode = (
94             get_info => "\x04",
95             search => "\x06",
96             online_members => "\x0b",
97             member_info => "\x0c",
98             grp_cmd_0x0f => "\x0f",
99             grp_cmd_0x19 => "\x19",
100             send_msg => "\x1a",
101             grp_cmd_0x36 => "\x36",
102             );
103              
104             our %GrpCmd;
105             foreach my $cmd (keys %GrpCmdCode) { $GrpCmd{$GrpCmdCode{$cmd}} = $cmd }
106              
107             # Bytes 0x05-0x06 form a packet sequence number, a 16-bit integer
108              
109             # Login modes
110             our %ConnectMode = (
111             Normal => "\x0a",
112             Away => "\x1e",
113             Invisible => "\x28"
114             );
115              
116             # System message code for 0x80 cmd
117             our %ServiceMsgCode = (
118             '01' => 'User',
119             '02' => 'ContactRequest',
120             '06' => 'Broadcast'
121             );
122              
123             # Separators
124             our $FS = "\x1e"; # Field separator
125             our $RS = "\x1f"; # Record separator
126              
127             our @InfoHeader = qw(
128             UserID Nickname Country Province PostCode Street Phone Age Sex Realname
129             Email PagerCode PagerProvider PagerStationNum PagerNum PagerType
130             Occupation Homepage Authorization unkn19 unkn20 Avatar
131             MobilePhone MobileType Aboutme City unkn26 unkn27 unkn28 PublishMobile
132             PublishContact School Horoscope Shengxiao BloodType unkn35 unkn36
133             );
134              
135             our %Emoticon = (
136             "\x41" => '¾ªÑÈ', "\x42" => 'Ʋ×ì', "\x43" => 'É«', "\x44" => '·¢´ô', "\x45" => 'µÃÒâ',
137             "\x46" => 'Á÷Àá', "\x47" => 'º¦Ðß', "\x48" => '±Õ×ì', "\x49" => '˯', "\x4a" => '´ó¿Þ',
138             "\x4b" => 'ÞÏÞÎ', "\x4c" => '·¢Å­', "\x4d" => 'µ÷Ƥ', "\x4e" => 'ßÚÑÀ', "\x4f" => '΢Ц',
139             "\x73" => 'Äѹý', "\x74" => '¿á', "\x75" => '·Çµä', "\x76" => '×¥¿ñ', "\x77" => 'ÍÂ',
140             "\x8a" => '', "\x8b" => '', "\x8c" => '', "\x8d" => '', "\x8e" => '',
141             "\x8f" => '', "\x78" => '', "\x79" => '', "\x7a" => '', "\x7b" => '',
142             "\x90" => '', "\x91" => '', "\x92" => '', "\x93" => '', "\x94" => '',
143             "\x95" => '', "\x96" => '', "\x97" => '', "\x98" => '', "\x99" => '',
144             "\x59" => '', "\x5a" => '', "\x5c" => '', "\x58" => '', "\x57" => '',
145             "\x55" => '', "\x7c" => '', "\x7d" => '', "\x7e" => '', "\x7f" => '',
146             "\x9a" => '', "\x9b" => '', "\x60" => '', "\x67" => '', "\x9c" => '',
147             "\x9d" => '', "\x9e" => '', "\x5e" => '', "\x9f" => '', "\x89" => '',
148             "\x80" => '', "\x81" => '', "\x82" => '', "\x62" => '', "\x63" => '',
149             "\x64" => '', "\x65" => '', "\x66" => '', "\x83" => '', "\x68" => '',
150             "\x84" => '', "\x85" => '', "\x86" => '', "\x87" => '', "\x6b" => '',
151             "\x6e" => '', "\x6f" => '', "\x70" => '', "\x88" => '', "\xa0" => '',
152             "\x50" => '', "\x51" => '', "\x52" => '', "\x53" => '', "\x54" => '',
153             "\x56" => '', "\x5b" => '', "\x5d" => '', "\x5f" => '', "\x61" => '',
154             "\x69" => 'ÏÂÓê', "\x6a" => '¶àÔÆ', "\x6c" => 'Ñ©ÈË', "\x6d" => 'ÐÇÐÇ', "\x71" => 'Å®',
155             "\x72" => 'ÄÐ'
156             );
157              
158             # Some constants for constructing client packets
159             my $PacketHead = STX . $CLIENT_VER;
160              
161             my $ProxyConnect = "CONNECT %s HTTP/1.1\r\nAccept: */*\r\nContent-Type: text/html\r\nProxy-Connection: Keep-Alive\r\nContent-length: 0\r\n\r\n";
162              
163             #################### End OICQ protocol data ########################
164              
165             # Constructor
166              
167             sub new {
168 0     0 0   my ($class) = @_;
169 0 0         my $homedir = exists($ENV{HOME}) ? $ENV{HOME} :
    0          
170             (exists($ENV{HOMEPATH}) ? $ENV{HOMEPATH} : '.');
171 0           my $dir = "$homedir/.oicq";
172 0 0         if (-e $dir) {
173 0 0         -d $dir or croak "$dir exists but is not a directory";
174             } else {
175 0 0         mkdir($dir) or croak "Failed to mkdir $dir: $!";
176             }
177 0           my $self = {
178             Dir => $dir,
179             LastSvrAck => 0,
180             Font => 'Tahoma',
181             FontSize => 12,
182             FontColor => '00a000',
183             Debug => 0 # 1 - trace packets, 2 - desect packets
184             };
185 0           my $logfile = "$dir/oicq.log";
186 0           my $log = new FileHandle ">>$logfile";
187 0 0         defined($log) or croak "Failed to open >>$logfile";
188 0           $log->autoflush;
189 0           $self->{LogFile} = $logfile;
190 0           $self->{Log} = $log;
191 0           return bless($self, $class);
192             }
193              
194             # Methods that do not require connection to a server
195              
196             sub set_user {
197 0     0 0   my ($self, $id, $pw) = @_;
198              
199 0           $self->{Id} = $id;
200 0           $self->{Passwd} = $pw;
201 0           $self->{_Id} = pack('N', $id);
202 0           $self->{PWKey} = Digest::MD5::md5(Digest::MD5::md5($pw));
203 0           $self->{EventQueue} = [];
204 0           $self->{EventQueueSize} = 50;
205 0           $self->{SearchCount} = 0;
206 0           $self->{LogChat} = 1;
207 0           $self->{Info} = {}; # use id as hash key
208 0           $self->{Away} = 0;
209 0           $self->{LastAutoReply} = {}; # use id as hash key
210 0           $self->{AutoAwayTime} = "";
211              
212 0           my $userdir = "$self->{Dir}/$id";
213 0 0         -e $userdir or mkdir($userdir);
214 0 0         if (-d $userdir) {
215 0           foreach ($self->get_saved_ids) { $self->get_nickname($_) };
  0            
216 0           my $logfile = "$userdir/user.log";
217 0           my $log = new FileHandle(">>$logfile");
218 0 0         if (defined $log) {
219 0 0         $self->log_t("Switch log to $logfile") if $self->{Debug};
220 0           $self->{Log} = undef;
221 0           $self->{LogFile} = $logfile;
222 0           $self->{Log} = $log;
223 0           $log->autoflush;
224             } else {
225 0           $self->log_t("Failed to open >>$logfile");
226             }
227             } else {
228 0           $self->log_t("Failed to mkdir $userdir");
229             }
230             }
231              
232             # Methods for building OICQ packets
233              
234             sub finalize_packet {
235 1     1   9 use bytes;
  1         2  
  1         4  
236 0     0 0   my ($self, $packet) = @_;
237 0 0         return($packet) if $self->{UDP};
238 0           return(pack('n', length($packet) + 2) . $packet);
239             }
240              
241             # A TCP packet from server may contain multiple QQ data segment, sometimes with
242             # null segments in the beginning, the end, or between commands.
243             # get_data method returns a list of valid QQ data segments, each of
244             # which generates a server event.
245              
246             sub get_data {
247 0     0 0   my ($self, $packet) = @_;
248 0 0         return () unless $packet;
249             # do nothing to UDP packets
250 0 0         return ($packet) if $self->{UDP};
251 0           my $len = length($packet);
252 0 0         if ($len < 10) { # 2 leading bytes + 7 bytes of header + 1 byte of tail(0x03)
253 0 0         $self->log_t("Discard short segment:\n", unpack("H*", $packet)) if $self->{Debug} > 8;
254 0           return ();
255             }
256 0           my $len1 = unpack('n', substr($packet, 0, 2));
257 0 0         return () if $len1 == 0; # TCP QQ packets must declare length in the beginning
258 0 0         if ($len1 <= $len) {
259 0 0 0       if (substr($packet, 2, 1) eq STX and substr($packet, $len1-1, 1) eq ETX) {
260 0           return(substr($packet, 2, $len1 - 2), get_data($self, substr($packet, $len1)));
261             }
262 0 0         $self->log_t("$len1 bytes discarded:\n", unpack("H*", substr($packet, 0, $len1))) if $self->{Debug} > 8;
263 0 0         return get_data($self, substr($packet, $len1)) if $len > $len1;
264 0           return ();
265             }
266 0 0         $self->log_t("Fragmented packet:\n", unpack("H*", $packet)) if $self->{Debug} > 8;
267 0           return ();
268             }
269              
270             # sub build_packet has been merged into sub send2svr
271              
272             sub rand_str {
273 0     0 0   my $len = pop;
274 0           join('', map(pack("C", rand(0xff)), 1..$len));
275             }
276              
277             sub build_login_request_packet {
278 0     0 0   my ($self, $step) = @_;
279 0 0         die "Invalid login request step: $step\n" unless $CmdCode{"login_request_$step"};
280 0           my $randkey = rand_str(16);
281             # Need to save it for decrypting server responses
282 0           $self->{"RandKey$step"} = $randkey;
283 0 0         my $data = $step == 1 ? "\0"x15 : "\1\0\5\0\0\0\0";
284 0           my $seq = pack('n', rand(0xff));
285 0           $self->{Seq} = unpack('n', $seq);
286 0           my $packet = $PacketHead . $CmdCode{"login_request_$step"} . $seq . $self->{_Id} .
287             $randkey . encrypt(undef, $data, $randkey) . ETX;
288 0           $self->finalize_packet($packet);
289             }
290              
291             sub build_login_packet {
292 0     0 0   my ($self, $server_response) = @_;
293              
294 0           my $randkey = rand_str(16);
295 0           $self->{RandKey} = $randkey;
296             # No change in seq number
297 0           my $data = encrypt(undef, "", $self->{PWKey}) . "\0"x19 .
298             #pack('H*', '09f9cce1f7e8502203cd7731deabfcda') .
299             pack('H*', '41d118ac147858f1d0814d7d7d7bd91f') .
300             #pack('H*', '01') .
301             pack('C', 0xc4) . #rand(0xff)) .
302             $ConnectMode{$self->{ConnectMode}} . "\0"x25 .
303             #pack('H*', '2447087cb1d3404cbda9037f36689e39') .
304             pack('H*', 'd7e27d1ab27e6346a70c4c0c3bd53256') .
305             #substr($server_response, 8, -1) .
306             substr($server_response, 5) .
307             #pack('H*', '0140011032a09700104fac17133afc7e8cfd1bd97d2613adc2') .
308             pack('H*', '01400175fda7bc00106b12f591b1d70bed46bbc3c23c663038') .
309             "\0"x5 . "\x06" . "\0"x19 .
310             pack('H*', '0299c281ae0010bb2673dcc29868b74cbc3f08cce01ea1') .
311             #(pack('H*', '00')x297);
312             "\0"x249;
313 0           my $packet = $PacketHead . $CmdCode{'login'} . pack('n', $self->{Seq}) .
314             $self->{_Id} . $randkey . encrypt(undef, $data, $randkey) . ETX;
315 0           $self->finalize_packet($packet);
316             }
317              
318             sub build_logout_packet {
319 0     0 0   my ($self) = @_;
320 0           my $packet = $PacketHead . $CmdCode{'logout'} . ("\xff" x 2) . $self->{_Id} .
321             encrypt(undef, $self->{PWKey}, $self->{Key}) . ETX;
322 0           $self->finalize_packet($packet);
323             }
324              
325             # Methods for logging and output
326              
327             sub log {
328 0     0 0   my $self = shift;
329 0           my $log = $self->{Log};
330 0           my $mesg = "@_";
331             #Encode::from_to($mesg, 'euc-cn', 'utf8');
332 0           print $log $mesg;
333             }
334              
335             sub logf {
336 0     0 0   my $self = shift;
337 0           my $log = $self->{Log};
338 0           my $mesg = "@_";
339             #Encode::from_to($mesg, 'euc-cn', 'utf8');
340 0           printf $log $mesg;
341             }
342              
343             sub log_t {
344 0     0 0   my ($self, @msg) = @_;
345 0           my $log = $self->{Log};
346 0           my $mesg = "@msg\n";
347             #Encode::from_to($mesg, 'euc-cn', 'utf8');
348 0           print $log substr(localtime, 4, 16), $mesg;
349             }
350              
351             sub hexdump {
352 0     0 0   my $str = pop;
353 0 0         return unless defined $str;
354 0           my $res = "";
355 0           my $len = length($str);
356 0           for (my $i = 0; $i < $len; $i += 16) {
357 0           my $s = substr($str, $i, 16);
358 0           my $hex = unpack('H*', $s);
359             #$s =~ s/[\x00-\x1f\x80-\x8f]/./g; # 0x00-0x1f will screw up terminal
360 0           $hex =~ s/(\w\w)/$1 /g;
361 0           $res .= $hex . "\n"; # sprintf("%-48s %s\n", $hex, $s);
362             }
363 0           $str =~ s/[\x00-\x1f]/./g;
364 0           return $res . $str . "\n";
365             }
366              
367             sub dump_substr {
368 0     0 0   my ($self, $data, $tmpl, $prefix, $begin, $len) = @_;
369 0           my ($str, $end);
370 0 0         if (defined($len)) {
371 0           $str = substr($data, $begin, $len);
372 0 0         $end = ($begin+$len < length($data)) ? $begin+$len-1 : length($data)-1;
373             } else {
374 0           $str = substr($data, $begin);
375 0           $end = length($data)-1;
376             }
377 0           $self->logf("0x%02x-0x%02x %s: ", $begin, $end, $prefix);
378 0 0         if ($tmpl =~ /\w/) {
379 0 0         if ($tmpl eq 'H*') {
380 0           $self->log("\n", $self->hexdump($str));
381             } else {
382 0           $self->log(unpack($tmpl, $str), "\n");
383             }
384             } else {
385 0           $self->log("$str\n");
386             }
387             }
388              
389             sub desect {
390 0     0 0   my $self = shift;
391 0 0         return unless $self->{Debug} > 1;
392 0           my $data = shift;
393 0           foreach my $arg (@_) {
394 0           $self->dump_substr($data, @{$arg});
  0            
395             }
396 0           return;
397             }
398              
399             sub show_address {
400 0     0 0   my ($self, $data) = @_;
401 0           my $ip = join('.', map(ord($_), split('', substr($data, 0, 4))));
402 0 0         return $ip unless length($data) >= 6;
403 0           my $port = unpack('n', substr($data, 4, 2));
404 0           return "$ip:$port";
405             }
406              
407             sub remove_saved_id {
408 0     0 0   my ($self, $id) = @_;
409 0           my $file = "$self->{Dir}/$self->{Id}/$id.dat";
410 0 0         if (-e $file) {
411 0           unlink($file);
412 0 0         return 0 if -e $file;
413 0           return 1;
414             } else {
415 0           return 0;
416             }
417             }
418              
419             sub get_saved_ids {
420 0     0 0   my ($self) = @_;
421 0           my $dir = "$self->{Dir}/$self->{Id}";
422 0           my @ids = ();
423 0 0         if (opendir(DIR, $dir)) {
424 0           while(my $f = readdir(DIR)) {
425 0 0         next unless $f =~ /^(\d+)\.dat$/;
426 0           push @ids, $1;
427             }
428 0           closedir(DIR);
429             }
430 0           return @ids;
431             }
432              
433             sub get_face {
434 0     0 0   my $num = pop;
435 0 0         return $num unless $num =~ /^\d+$/;
436 0           sprintf('%d-%d', 1 + $num/3, 1 + $num % 3);
437             }
438              
439             sub toggle_autoreply {
440 0     0 0   my ($self) = @_;
441 0 0         if ($self->{Away}) {
442 0           $self->{Away} = 0;
443 0           return "off";
444             } else {
445 0           $self->{Away} = 1;
446 0           return "on";
447             }
448             }
449              
450             # Nickname can be updated by get_friends_list or get_user_info
451              
452             sub get_nickname {
453 0     0 0   my ($self, $id) = @_;
454 0 0         if (defined $self->{Info}->{$id}) {
455 0 0         if (defined $self->{Info}->{$id}->{Nickname}) {
456 0           return $self->{Info}->{$id}->{Nickname};
457             }
458             } else {
459 0           $self->{Info}->{$id} = {};
460             }
461 0           my $infofile = "$self->{Dir}/$self->{Id}/$id.dat";
462 0           my $nick = "";
463 0 0         if (open(INFO, $infofile)) {
464 0           while(my $line = ) {
465 0 0         if ($line =~ /^Nickname +=> *'(.*)'/) {
466 0           $nick = $1;
467 0           last;
468             }
469             }
470 0           close(INFO);
471             }
472 0           $self->{Info}->{$id}->{Nickname} = $nick;
473 0           return $nick;
474             }
475              
476             sub get_servers {
477 0     0 0   my @servers;
478 0 0 0       if (exists $ENV{OICQ_SVR} and $ENV{OICQ_SVR} =~ /\w+/) {
479 0           my $svr = $ENV{OICQ_SVR};
480 0           $svr =~ s/^\W+//;
481 0           $svr =~ s/\W+$//;
482 0           @servers = split(/[^\w\.]+/, $svr);
483 0 0         return @servers if @servers;
484             }
485              
486 0           my $type = pop;
487 0 0         if ($type =~ /udp/i) {
488 0           map {'sz'. $_ . '.' . $SERVER_DOMAIN} (2 .. 9, '');
  0            
489             } else {
490 0           map {'tcpconn' . $_ . '.' . $SERVER_DOMAIN} (6, 5, 4, 3, 2, '');
  0            
491             }
492             }
493              
494             sub tcp_connect {
495 0     0 0   my ($self, $server, $proxy) = @_;
496 0           my ($svr_ip, $svr_port);
497 0 0         if ($server =~ /^(\S+):(\d+)$/) {
498 0           ($svr_ip, $svr_port) = ($1, $2);
499             } else {
500 0           $svr_ip = $server;
501 0           $svr_port = 443;
502             }
503 0           my $socket;
504 0 0         $proxy = $ENV{OICQ_PROXY} unless defined $proxy;
505 0 0         if ($proxy) {
506 0           my ($proxy_ip, $proxy_port);
507 0 0         if ($proxy =~ /:/) {
508 0           ($proxy_ip, $proxy_port) = split(/:/, $proxy);
509             } else {
510 0           $proxy_ip = $proxy;
511 0           $proxy_port = 80;
512             }
513 0           $socket = IO::Socket::INET->new(
514             Proto => 'tcp', PeerAddr => $proxy_ip, PeerPort => $proxy_port
515             );
516 0 0         unless(defined $socket) {
517 0           $self->mesg("socket error: $@");
518 0           return;
519             }
520 0           $self->{Socket} = $socket;
521 0           $socket->send(sprintf $ProxyConnect, "$svr_ip:$svr_port");
522 0           my $resp = $self->timed_recv(0x4000, 10);
523 0 0 0       if (defined $resp && $resp =~ m|HTTP/.+ 200 Connection established|) {
524 0           $self->mesg("via proxy $proxy_ip:$proxy_port ");
525 0           $self->{Proxy} = "$proxy_ip:$proxy_port";
526 0           $self->{SvrIP} = $svr_ip;
527 0           $self->{SvrPort} = $svr_port;
528 0           $self->{Socket} = $socket;
529 0           $self->{UDP} = 0;
530 0           return $socket;
531             }
532 0 0         $resp = "" unless defined $resp;
533 0           $self->mesg("failed to connect to proxy $proxy_ip:$proxy_port\n$resp\n");
534 0           return;
535             } else {
536 0           $socket = IO::Socket::INET->new(
537             Proto => 'tcp', PeerAddr => $svr_ip, PeerPort => $svr_port
538             );
539 0 0         unless(defined $socket) {
540 0           $self->mesg("socket error: $@");
541 0           return;
542             }
543 0           $self->{SvrIP} = $svr_ip;
544 0           $self->{SvrPort} = $svr_port;
545 0           $self->{Socket} = $socket;
546 0           $self->{UDP} = 0;
547 0           return $socket;
548             }
549             }
550              
551             sub timed_recv {
552 0     0 0   my ($self, $length, $timeout) = @_;
553 0           my $socket = $self->{Socket};
554 0           my $timeout_msg = "tImEoUt\n";
555 0           my $res;
556 0     0     local $SIG{ALRM} = sub { die $timeout_msg };
  0            
557 0           alarm($timeout);
558 0           eval { $socket->recv($res, $length, 0); alarm(0) };
  0            
  0            
559 0 0         if ($@ eq $timeout_msg) {
560 0           return;
561             }
562 0           return $res;
563             }
564              
565             sub udp_connect {
566 0     0 0   my ($self, $server) = @_;
567 0 0         croak "Server IP not provided\n" unless defined($server);
568 0           my $port = 8000;
569              
570 0           my $socket = IO::Socket::INET->new(
571             Proto => 'udp', PeerAddr => $server, PeerPort => $port
572             );
573 0 0         unless(defined $socket) {
574 0           $self->mesg("socket error: $@");
575 0           return;
576             }
577 0           $self->{SvrIP} = $server;
578 0           $self->{SvrPort} = $port;
579 0           $self->{Socket} = $socket;
580 0           $self->{UDP} = 1;
581 0           return $socket;
582             }
583              
584             sub connect {
585 0     0 0   my $self = shift;
586 0           my $proto = shift;
587 0 0         ($proto eq 'udp') ? $self->udp_connect(@_) : $self->tcp_connect(@_);
588             }
589              
590             sub login {
591 0     0 0   my ($self, $id, $pw, $mode, $proto, $proxy) = @_;
592 0           $self->set_user($id, $pw);
593 0           $self->{Key} = "";
594 0           $| = 1;
595              
596 0 0 0       if (defined $mode && exists $ConnectMode{$mode}) {
597 0           $self->log_t("login as $id in $mode mode");
598 0           $self->{ConnectMode} = $mode;
599             } else {
600 0           $self->log_t("login as $id, default to invisible mode");
601 0           $self->{ConnectMode} = 'Invisible';
602             }
603             # Default to tcp connection
604 0 0 0       $proto = 'tcp' unless defined($proto) && $proto eq 'udp';
605 0           my @servers = $self->get_servers($proto);
606 0           my $login_packet;
607 0           SVR: foreach my $svr (@servers) {
608 0           $self->mesg("Connecting to $proto server $svr...");
609 0           my $socket = $self->connect($proto, $svr, $proxy);
610 0 0         next SVR unless defined $socket;
611 0 0         $self->mesg("socket created...") if $self->{Debug};
612              
613 0 0         unless ($login_packet) {
614 0           my $token = $self->get_login_token($svr, $proto, $proxy);
615 0 0         next SVR unless $token;
616 0           $login_packet = $self->build_login_packet($token);
617             }
618 0           my $plain = $self->decrypt_login_response($login_packet);
619 0 0         unless(defined $plain) {
620 0           $login_packet = undef;
621 0           next SVR;
622             }
623 0 0         $self->mesg("decrypted login resp: ", unpack("H*", $plain), "\n") if $self->{Debug};
624 0           my $login = ord($plain);
625 0 0 0       if ($login == 0) { # login successfull
    0          
    0          
    0          
626 0           $self->{Key} = substr($plain, 1, 0x10);
627 0           $self->{Addr} = $self->show_address(substr($plain, 0x15, 6));
628 0           $self->{LoginTime} = unpack('N', substr($plain, 0x21, 4));
629 0           $self->{Addr2} = $self->show_address(substr($plain, 0x7b, 4));
630 0           $self->{LoginTime2} = unpack('N', substr($plain, 0x7f, 4));
631 0           $self->mesg("ok.\n");
632 0           last SVR;
633             } elsif ($login == 1) { # redirect to another server
634 0           $svr = $self->show_address(substr($plain, 5, 6));
635 0           ($self->{SvrIP}, $self->{SvrPort}) = split(/:/, $svr);
636 0           $self->{Socket} = undef;
637 0           $self->log_t("redirected to server $svr");
638 0           $self->mesg(" redirected.\n");
639 0           redo SVR;
640             } elsif ($login == 9 or $login == 5) { # wrong password
641 0           $self->mesg("$plain\nError code $login\n");
642 0           last SVR;
643             } elsif ($login == 10) { # redirect to another server
644 0           $svr = $self->show_address(substr($plain, -4));
645 0           $self->mesg("redirected to server $svr (code $login).\n");
646 0           $self->{SvrIP} = $svr;
647 0           $self->{Socket} = undef;
648 0           $socket = undef;
649 0           redo SVR;
650             } else {
651 0           my $h = unpack("H*", $plain);
652 0           $self->mesg("failed with error code $login\n$h\n");
653 0           last SVR;
654             }
655             }
656              
657 0 0         return 0 unless $self->{Key};
658              
659             # Make sure we logout when control-C is pressed
660 0     0     $SIG{INT} = sub { $self->logout; exit 1 };
  0            
  0            
661             # Prepare LogoutPacket for logout
662 0           $self->{LogoutPacket} = $self->build_logout_packet;
663 0           $self->{LastKeepaliveTime} = time;
664              
665 0           return 1;
666             }
667              
668             sub get_login_token {
669 0     0 0   my ($self) = @_;
670 0           my $socket = $self->{Socket};
671 0 0         return unless defined $socket;
672 0 0         $self->mesg("socket created...") if $self->{Debug};
673 0           my ($login_req, $resp);
674 0           foreach my $step (2) {
675 0           $login_req = $self->build_login_request_packet($step);
676 0           $socket->send($login_req);
677 0 0         $self->mesg("waiting for token $step...") if $self->{Debug};
678 0           $resp = $self->timed_recv(1024, 5);
679 0 0         if (defined $resp) {
680 0 0         $self->mesg("received...") if $self->{Debug};
681             } else {
682 0           $self->mesg("timed out.\n");
683 0           return;
684             }
685             }
686             #foreach (1 .. 8) {
687             # $socket->send($login_req);
688             #}
689 0           my $token;
690 0           foreach my $r ($self->get_data($resp)) {
691 0 0         next unless substr($r, 3, 2) eq $CmdCode{login_request_2};
692 0           eval { $token = decrypt(undef, substr($r, 7, -1), $self->{RandKey2}) };
  0            
693 0 0         $self->mesg("token:", unpack("H*", $token)) if $self->{Debug};
694 0 0         return($token) if $token;
695             }
696              
697 0           $self->mesg("unexpected server response to login request:\n",
698             unpack('H*', $resp), "\n$resp\n");
699 0           return;
700             }
701              
702             sub decrypt_login_response {
703 0     0 0   my ($self, $login_packet) = @_;
704 0           $self->{Socket}->send($login_packet);
705 0           $self->mesg("login packet sent ...");
706 0           my $data;
707 0           RECV: while (1) {
708 0           my $resp = $self->timed_recv(4096, 5);
709 0 0         unless($resp) {
710 0           $self->mesg(" no response.\n");
711 0           return;
712             }
713 0           foreach my $d ($self->get_data($resp)) {
714 0 0         $self->mesg("\nServer response:", unpack("H*", $d), "\n") if $self->{Debug};
715 0 0         if (substr($d, 3, 2) eq "\x00\x22") {
716 0           $data = $d;
717 0           last RECV;
718             }
719             }
720             }
721 0           $self->{LastSvrAck} = time;
722             #my ($data) = $self->get_data($resp);
723             #return unless defined $data;
724 0           my $crypt = substr($data, 7, -1);
725 0           my $plain;
726 0 0         $self->mesg("received ", length($crypt), " bytes...") if $self->{Debug};
727 0 0         my @keys = length($crypt) == 32 ? qw(RandKey PWKey) : qw(PWKey RandKey);
728 0           foreach my $key (@keys) {
729 0           eval { $plain = decrypt(undef, $crypt, $self->{$key}) };
  0            
730 0 0         if (defined $plain) {
731 0 0         $self->mesg("decrypted with $key\n") if $self->{Debug};
732 0           return $plain;
733             }
734 0 0 0       $self->mesg("Failed to decrypt login response: $@") if $@ && $self->{Debug};
735             }
736 0           return undef;
737             }
738              
739             sub mesg {
740 0     0 0   my ($self, @mesg) = @_;
741 0           my $mesg = "@mesg";
742 0 0 0       if (exists($ENV{LANG}) and $ENV{LANG} =~ /UTF-8/) {
743 0           Encode::from_to($mesg, 'euc-cn', 'utf8');
744             }
745 0           print $mesg;
746             }
747              
748             # send2svr may take command Seq num as an optional argument
749             # it returns a Net::OICQ::ClientEvent object if the packet is sent
750              
751             sub send2svr {
752 0     0 0   my ($self, $cmd, $data, $seq) = @_;
753 0 0         croak "send2svr error: bad command: $cmd" unless exists $CmdCode{$cmd};
754 0 0         unless(defined $seq) {
755 0           $seq = pack('n', ++$self->{Seq});
756             }
757 0           my $header = $PacketHead . $CmdCode{$cmd} . $seq . $self->{_Id};
758 0           my $crypt = encrypt(undef, $data, $self->{Key});
759 0           my $packet = $self->finalize_packet("$header$crypt" . ETX);
760 0 0         if ($self->{Socket}->send($packet)) {
761 0           return(new Net::OICQ::ClientEvent($header, $data, $self));
762             }
763 0           return undef;
764             }
765              
766             # get_friends_list provided by Chen Peng
767              
768             sub get_friends_list {
769 0     0 0   my ($self, $flag) = @_;
770 0 0         defined $flag or $flag = pack('H4', '0000');
771 0           $self->send2svr('get_friends_list', $flag);
772             }
773              
774             sub get_online_friends {
775 0     0 0   my ($self) = @_;
776 0           $self->send2svr('get_online_friends', pack('H*', '0200000000'));
777             }
778              
779             sub set_mode {
780 0     0 0   my ($self, $mode_code) = @_;
781 0           $self->send2svr('set_mode', $mode_code);
782             }
783              
784             sub get_user_info {
785 0     0 0   my ($self, $id) = @_;
786 0           $self->send2svr('get_user_info', $id);
787             }
788              
789             sub update_info {
790 0     0 0   my ($self, $hashref) = @_;
791 0           my $info = $self->{MyInfo};
792 0 0 0       return unless defined $hashref and defined $info;
793 0           my %new_info;
794             # Use all upper-case letters for keys
795 0           foreach my $k (keys %$hashref) {
796 0           $new_info{uc($k)} = $hashref->{$k};
797             }
798 0           my @update;
799 0           for (my $i = 1; $i < $#InfoHeader; $i++) {
800 0           my $attr = uc($InfoHeader[$i]);
801 0 0         push(@update, defined($new_info{$attr}) ? $new_info{$attr} : $info->[$i]);
802             }
803 0           $self->send2svr('update_info', join($RS, "", "", @update));
804             }
805              
806             sub set_passwd {
807 0     0 0   my ($self, $newpw) = @_;
808 0 0         return unless defined $self->{MyInfo};
809 0           my @info = @{$self->{MyInfo}};
  0            
810 0           pop @info; shift @info;
  0            
811 0           $self->send2svr('update_info', join($RS, $self->{Passwd}, $newpw, @info));
812             }
813              
814             sub accept_contact {
815 0     0 0   my ($self, $id) = @_;
816 0           $self->send2svr('add_contact_2', $id.$RS."0");
817             }
818              
819             sub reject_contact {
820 0     0 0   my ($self, $id) = @_;
821 0           $self->send2svr('add_contact_2', $id.$RS."1");
822             }
823              
824             sub add_contact {
825 0     0 0   my ($self, $id) = @_;
826 0           $self->send2svr('add_contact_1', "$id");
827             }
828              
829             sub add_contact_2 {
830 0     0 0   my ($self, $id, $msg) = @_;
831 0           $self->send2svr('add_contact_2', "$id$RS"."2$RS$msg");
832             }
833              
834             sub del_contact {
835 0     0 0   my ($self, $id) = @_;
836 0           $self->send2svr('del_contact', "$id");
837             }
838              
839             sub forbid_contact {
840 0     0 0   my ($self, $id) = @_;
841 0           $self->send2svr('forbid_contact', "$id");
842             }
843              
844             sub msg_tail {
845 0     0 0   my ($self) = @_;
846 0           my $font_name = $self->{Font};
847             # Let's have fun with font size and color
848 0           my $font_size = $self->{FontSize};
849 0           my $font_color = $self->{FontColor};
850 0 0         if ($font_size =~ /^\d+$/) {
851 0           $font_size = chr($font_size);
852             } else {
853 0           $font_size = chr(8+rand(14));
854             }
855 0 0         if ($font_color =~ /^[\da-f]{6}$/) {
856 0           $font_color = pack("H*", $font_color);
857             } else {
858 0           $font_color = chr(rand(0xff)).chr(rand(0xff)).chr(rand(0xff));
859             }
860 0           my $msg_tail = " \0$font_size$font_color\0\x86\x02$font_name";
861             # Don't know what would happen if font_name is very looooong. Don't care either.
862 0           return $msg_tail . chr(length($msg_tail));
863             }
864              
865             # send_msg is also used for auto-reply
866             # I don't think this is a bug, it is a feature.
867             sub send_msg {
868 0     0 0   my ($self, $dstid, $msg) = @_;
869 1     1   8227 use bytes;
  1         3  
  1         9  
870 0           my $nickname = $self->get_nickname($dstid);
871 0 0 0       if ($dstid =~ /^20/ and $nickname eq "\xc8\xba") {
872             # Group message
873 0           return $self->send_group_msg($dstid, $msg);
874             }
875 0 0         $self->log_t("Sent message to $dstid:\n", $msg) if $self->{LogChat};
876 0           my $dstid_ = pack('N', $dstid);
877 0           my $head = $self->{_Id} . $dstid_ . $CLIENT_VER . $self->{_Id} . $dstid_ .
878             Digest::MD5::md5($dstid_ . $self->{Key}) . "\0\x0b";
879 0           my @trunks = $self->split_gb_msg($msg);
880 0           my $last_trunk = pop(@trunks);
881 0           my $msg_seq = 0x57 + rand(0xa8);
882 0           my $time = pack('N', time);
883 0           foreach my $trunk (@trunks) {
884 0           my $data = $head . pack('n', ++$msg_seq) . $time .
885             "\0\x3f\0\0\0\1\1\0" . chr(rand(0xfd)) . "\0\1" . $trunk;
886 0           $self->send2svr('send_msg', $data);
887 0           sleep(1);
888             }
889 0           my $data = $head . pack('n', ++$msg_seq) . $time .
890             "\0\x3f\0\0\0\1\1\0" . chr(rand(0xfd)) . "\0\1" .
891             $last_trunk . $self->msg_tail;
892 0           $self->send2svr('send_msg', $data);
893             }
894              
895             # Server will not send message longer than 601 bytes
896              
897             sub split_gb_msg {
898 0     0 0   my ($self, $msg) = @_;
899 0           my $len = length($msg);
900 0           my $max_len = 601;
901 0 0         return ($msg) if $len <= $max_len;
902 0           my $msg0 = substr($msg, 0, $max_len);
903             # here is my idea of splitting a long messages while avoiding breaking up
904             # any GB character
905             # First, count the non GB characters in the first 601 characters
906 0           my $non_gb_count = $msg0 =~ tr/\x00-\xa0/\x00-\xa0/;
907 0 0         if ($non_gb_count % 2) {
908             # if there are an odd number of non GB characters,
909             # it's ok to break at position 601
910 0           return ($msg0, $self->split_gb_msg(substr($msg, $max_len)));
911             } else {
912 0           $max_len--;
913 0           return (substr($msg, 0, $max_len), $self->split_gb_msg(substr($msg, $max_len)));
914             }
915             }
916              
917             sub ack_msg {
918 0     0 0   my ($self, $seq, $plain) = @_;
919 0           $plain = substr($plain, 0, 16);
920 0           my $event = $self->send2svr('recv_msg', $plain, $seq);
921 0 0         if ($self->{UDP}) {
922 0           foreach (1..2) {
923 0           $self->send2svr('recv_msg', $plain, $seq);
924             }
925             }
926 0           return $event;
927             }
928              
929             sub ack_service_msg {
930 0     0 0   my ($self, $code, $srcid, $seq) = @_;
931 0           $self->send2svr('ack_service_msg', "$code$FS$srcid$FS$seq");
932             }
933              
934             sub keepalive {
935 0     0 0   my ($self) = @_;
936 0           $self->{LastKeepaliveTime} = time;
937 0           $self->send2svr('keep_alive', $self->{Id});
938             }
939              
940             sub search_user {
941 0     0 0   my ($self, $id) = @_;
942 0           $self->send2svr('search_users', join($RS, '0', $id, '-','-','0'));
943             }
944              
945             sub list_online_users {
946 0     0 0   my ($self, $num) = @_;
947 0 0         defined $num or $num = 1;
948 0           my $begin = $self->{SearchCount};
949 0           $self->{SearchCount} += $num;
950 0           my $end = $self->{SearchCount} -1;
951 0           foreach my $p ($begin .. $end) {
952 0           $self->send2svr('search_users', "1".$RS."$p");
953             }
954             }
955              
956             sub request_file_key {
957 0     0 0   my ($self, $hex_code) = @_;
958 0           $self->send2svr('req_file_key', pack("H*", $hex_code));
959             }
960              
961             sub do_group {
962 0     0 0   my ($self, $group_cmd, $group_id, $what) = @_;
963 0           my $data = $GrpCmdCode{$group_cmd};
964 0 0         $data .= pack('H2', '01') if $group_cmd eq 'search';
965 0           $data .= pack('N', $group_id) . $what;
966 0           $self->send2svr('do_group', $data);
967             }
968              
969             # Group functions are provided by alexe
970              
971             sub send_group_msg {
972 0     0 0   my ($self, $group_id, @msg) = @_;
973 0           my $mesg = "@msg";
974 0 0         $self->log_t("Sent message to Group $group_id:\n", $mesg) if $self->{LogChat};
975 0           my $group_int_id = $self->group_int_id($group_id);
976 0           my @trunks = $self->split_gb_msg($mesg);
977 0           my $last_trunk = pop(@trunks);
978 0           foreach my $trunk (@trunks) {
979 0           my $data = "\0\1\1\0\x39\xe8\0\0\0\0$trunk";
980 0           $data = pack('n', length($data)) . $data;
981 0           $self->do_group('send_msg', $group_int_id, $data);
982 0           sleep(1);
983             }
984 0           my $data = "\0\1\1\0\x39\xe8\0\0\0\0$last_trunk" . $self->msg_tail;
985 0           $data = pack('n', length($data)) . $data;
986 0           $self->do_group('send_msg', $group_int_id, $data);
987             }
988              
989             sub get_group_info {
990 0     0 0   my ($self, $group_id) = @_;
991 0           $self->do_group('get_info', $self->group_int_id($group_id), "");
992             }
993              
994             sub search_group {
995 0     0 0   my($self, $group_id) = @_;
996 0           $self->do_group('search', $group_id, "");
997             }
998              
999             sub group_online_members {
1000 0     0 0   my ($self, $group_id) = @_;
1001 0           $self->do_group('online_members', $self->group_int_id($group_id), "");
1002             }
1003              
1004             sub group_int_id {
1005 0     0 0   my ($self, $group_id) = @_;
1006 0 0         $group_id += 202000000 if $group_id < 202000000;
1007 0           return $group_id;
1008             }
1009              
1010             sub logout {
1011 0     0 0   my $self = shift;
1012 0 0 0       defined($self->{LogoutPacket}) && $self->{LogoutPacket} || return;
1013 0           my $packet = $self->{LogoutPacket};
1014 0           foreach (1..3) {
1015 0           $self->{Socket}->send($packet);
1016             }
1017             }
1018              
1019             1;
1020              
1021             __END__