File Coverage

blib/lib/Net/RGTP.pm
Criterion Covered Total %
statement 266 326 81.6
branch 87 130 66.9
condition 20 28 71.4
subroutine 24 26 92.3
pod 12 12 100.0
total 409 522 78.3


line stmt bran cond sub pod time code
1             # Net::RGTP -*- cperl -*-
2             #
3             # This program is free software; you may distribute it under the same
4             # conditions as Perl itself.
5             #
6             # Copyright (c) 2005 Thomas Thurman
7              
8             ################################################################
9              
10             package Net::RGTP;
11              
12 2     2   71363 use strict;
  2         5  
  2         61  
13 2     2   10 use warnings;
  2         4  
  2         55  
14 2     2   12 use vars qw(@ISA $VERSION);
  2         7  
  2         146  
15              
16 2     2   2144 use Socket 1.3;
  2         11150  
  2         1267  
17 2     2   2699 use IO::Socket;
  2         77411  
  2         8  
18 2     2   5058 use Net::Cmd;
  2         11946  
  2         179  
19 2     2   18 use Digest::MD5 qw(md5_hex);
  2         4  
  2         144  
20              
21             $VERSION = '0.10';
22             @ISA = qw(Exporter Net::Cmd IO::Socket::INET);
23              
24 2     2   11 use constant GROGGS => 'rgtp-serv.groggs.group.cam.ac.uk';
  2         3  
  2         142  
25 2     2   8 use constant RGTP => 'rgtp(1431)';
  2         4  
  2         6670  
26              
27             ################################################################
28              
29             sub new
30             {
31 3     3 1 2091 my $package = shift;
32 3         14 my %args = @_;
33              
34 3 50 50     93 my $self = $package->SUPER::new(PeerAddr => $args{Host} || GROGGS,
    50 50        
35             PeerPort => $args{Port} || RGTP,
36             LocalAddr => $args{'LocalAddr'},
37             Proto => 'tcp',
38             Timeout => defined $args{Timeout}?
39             $args{Timeout}: 120
40             ) or return undef;
41              
42 3 50       373453 $self->debug(100) if $args{'Debug'};
43              
44 3 50       37 unless ($self->response) {
45 0         0 $@ = "Couldn't get a response from the server";
46 0         0 return undef;
47             }
48              
49 3         20007 ${*$self}{'net_rgtp_groggsbug'} = $self->message =~ /GROGGS system/;
  3         46  
50            
51 3 50 33     23 if ($self->code()<230 || $self->code()>232) {
52 0         0 $@ = "Not an RGTP server";
53 0         0 return undef;
54             }
55              
56 3         94 $self->_set_alvl;
57              
58 3         18 $self;
59             }
60              
61             sub access_level {
62 4     4 1 699 my $self = shift;
63              
64 4         7 return ${*$self}{'net_rgtp_status'};
  4         33  
65             }
66              
67             sub latest_seq {
68 0     0 1 0 my $self = shift;
69              
70 0         0 return ${*$self}{'net_rgtp_latest'};
  0         0  
71             }
72              
73             sub motd {
74 1     1 1 2616 my $self = shift;
75              
76 1         9 $self->command('MOTD');
77 1         207 $self->_read_item(no_parse_headers=>1,
78             motd=>1);
79             }
80              
81             sub item {
82 3     3 1 3292 my ($self, $itemid) = @_;
83              
84 3 50       15 return $self->motd if $itemid eq 'motd';
85              
86 3 50       14 return undef unless _is_valid_itemid($itemid);
87              
88 3         18 $self->command('ITEM', $itemid);
89 3         584 $self->_read_item;
90             }
91              
92             sub quick_item {
93 3     3 1 2052 my ($self, $itemid) = @_;
94              
95 3 50       13 return $self->motd if $itemid eq 'motd';
96              
97 3         9 my %result = ();
98              
99 3         15 $self->command('STAT', $itemid);
100 3         979 $self->response;
101              
102 3         31334 my ($parent, $child, $edit, $reply, $subject) =
103             $self->message =~ /^([A-Za-z]\d{7}|\s{8}) ([A-Za-z]\d{7}|\s{8}) ([0-9a-fA-F]{8}|\s{8}) ([0-9a-fA-F]{8}) (.*)$/;
104              
105 3 100       78 $result{'parent' } = $parent if $parent ne ' ';
106 3 100       16 $result{'child' } = $child if $child ne ' ';
107 3 50       15 $result{'edit' } = hex($edit) if $edit ne ' ';
108 3         85 $result{'reply' } = hex($reply);
109 3         10 $result{'subject'} = $subject;
110              
111 3         67 \%result;
112             }
113              
114             sub login {
115 3     3 1 702 my ($self, $userid, $secret) = @_;
116              
117 3   100     18 $userid ||= 'guest';
118              
119 3         30 $self->command('USER', $userid);
120 3         581 $self->response;
121              
122             # Did they let us in for just saying who we were?
123 3 100 66     40205 if ($self->code >= 230 && $self->code <= 233) {
124 1 50       34 if (defined $secret) {
125 0         0 $@ = 'Unexpected lack of security-- possible man in the middle attack?';
126 0         0 return undef;
127             }
128              
129 1         8 return $self->_set_alvl;
130             }
131              
132 2 50       48 if ($self->code eq '500') {
133 0         0 $@ = 'Already logged in';
134 0         0 return undef;
135             }
136 2         45 $self->_expect_code('130');
137              
138 2         29 my ($algorithm) = $self->message =~ /^(.*?) /;
139 2 50       43 if ($algorithm eq 'MD5') {
140 2         14 $@ = "Unknown algorithm: $algorithm";
141             }
142              
143 2         26 $self->response;
144 2         58059 $self->_expect_code('333');
145 2         73 my ($server_nonce) = $self->message =~ /([a-zA-Z0-9]{32})/;
146 2         57 $server_nonce = pack("H*", $server_nonce);
147              
148 2         8 $secret = pack("H*", $secret);
149              
150 2         5 my $flipped_secret = '';
151 2         14 for (my $i=0; $i
152 16         45 $flipped_secret .= chr((~ord(substr($secret,$i,1)) & 0xFF));
153             }
154              
155 2         7 my $munged_userid = substr($userid, 0, 16);
156 2         9 while (length($munged_userid)<16) {
157 0         0 $munged_userid .= chr(0);
158             }
159              
160 2         49 my $client_nonce = '';
161 2         10 for (my $i=0; $i<16; $i++) {
162 32         204 $client_nonce .= chr(int(rand(256)));
163             }
164              
165 2         18 my $client_hash = md5_hex($client_nonce,
166             $server_nonce,
167             $munged_userid,
168             $flipped_secret);
169            
170 2         10 my $server_hash = md5_hex($server_nonce,
171             $client_nonce,
172             $munged_userid,
173             $secret);
174            
175             # Now we prove to the server that we know the secret...
176            
177 2         21 $self->command('AUTH', $client_hash, unpack('H*',$client_nonce));
178              
179             # ...and it proves the same to us.
180              
181 2         441 $self->response;
182            
183 2 50       19689 unless ($server_hash eq substr(lc($self->message), 0, 32)) {
184 0         0 $@ = "server failed to authenticate to us";
185 0         0 return undef;
186             }
187              
188 2         67 $self->response;
189              
190 2         105356 return $self->_set_alvl;
191             }
192              
193             sub items {
194 1     1 1 3 my $self = shift;
195 1   50     2 my $latest_seq = ${*$self}{'net_rgtp_latest'} || 0;
196              
197 1 50       2 if (defined ${*$self}{'net_rgtp_latest'}) {
  1         7  
198 0         0 $self->command('INDX', sprintf('#%08x', ${*$self}{'net_rgtp_latest'}+1));
  0         0  
199             } else {
200 1         6 $self->command('INDX');
201 1         339 ${*$self}{'net_rgtp_index'} = {};
  1         4  
202             }
203              
204 1         5 $self->response;
205              
206 1 50       10051 if ($self->code eq '531') {
207 0         0 $@ = 'No reading access';
208 0         0 return undef;
209             }
210 1         19 $self->_expect_code('250');
211              
212 1         10 for my $line (@{$self->read_until_dot}) {
  1         16  
213 9363         515948 my $seq = hex(substr($line, 0, 8));
214 9363         12901 my $timestamp = hex(substr($line, 9, 8));
215 9363         12511 my $itemid = substr($line, 18, 8);
216 9363         15570 my $from = substr($line, 27, 75);
217 9363         11305 my $type = substr($line, 103, 1);
218 9363         14727 my $subject = substr($line, 105);
219            
220 9363         52940 $from =~ s/\s*$//;
221 9363         88160 $subject =~ s/\s*$//;
222              
223 9363 100       20703 if ($type eq 'M') {
224 32         49 $itemid = 'motd';
225 32         40 $subject = 'Message of the Day';
226 32         35 $type = 'I';
227             }
228              
229 9363 100       22893 if ($type eq 'C') {
    100          
230 789         734 ${*$self}{'net_rgtp_childlink'} = $itemid;
  789         2301  
231             } elsif ($type eq 'F') {
232 787 50       700 if (defined ${*$self}{'net_rgtp_childlink'}) {
  787         2572  
233 787         2252 ${*$self}{'net_rgtp_index'}
  787         1735  
234 787         725 { ${*$self}{'net_rgtp_childlink'} }{'child'} = $itemid;
235 787         1912 ${*$self}{'net_rgtp_index'}
  787         1287  
236 787         1052 { $itemid }{'parent'} = ${*$self}{'net_rgtp_childlink'};
237 787         932 delete ${*$self}{'net_rgtp_childlink'};
  787         1741  
238             }
239             }
240            
241 9363 100 100     30496 if ($type eq 'R' or $type eq 'I' or $type eq 'C') {
      100        
242 8557         8332 ${*$self}{'net_rgtp_index'}{ $itemid }{'subject'} = $subject;
  8557         27637  
243 8557         16658 ${*$self}{'net_rgtp_index'}{ $itemid }{'posts'}++;
  8557         17795  
244 8557         10143 ${*$self}{'net_rgtp_index'}{ $itemid }{'timestamp'} = $timestamp;
  8557         17142  
245 8557         10185 ${*$self}{'net_rgtp_index'}{ $itemid }{'seq'} = $seq;
  8557         18005  
246             }
247              
248 9363 100       26193 $latest_seq = $seq if $seq > $latest_seq;
249            
250             }
251              
252 1         1259 ${*$self}{'net_rgtp_latest'} = $latest_seq;
  1         14  
253              
254 1         2 ${*$self}{'net_rgtp_index'};
  1         9  
255              
256             }
257              
258             sub state {
259 0     0 1 0 my ($self, $setting) = @_;
260              
261 0 0       0 if (defined $setting) {
262 0 0       0 if (defined $setting->{'latest'}) {
263 0         0 ${*$self}{'net_rgtp_latest'} = $setting->{'latest'};
  0         0  
264 0         0 ${*$self}{'net_rgtp_index'} = $setting->{'index'};
  0         0  
265             } else {
266 0         0 delete ${*$self}{'net_rgtp_latest'};
  0         0  
267 0         0 delete ${*$self}{'net_rgtp_index'};
  0         0  
268             }
269             } else {
270 0 0       0 if (defined ${*$self}{'net_rgtp_latest'}) {
  0         0  
271             return {
272 0         0 latest => ${*$self}{'net_rgtp_latest'},
  0         0  
273 0         0 index => ${*$self}{'net_rgtp_index'},
274             };
275             } else {
276             return {
277 0         0 index => {},
278             };
279             }
280             }
281             }
282              
283             sub post {
284              
285 12     12 1 49 my ($self, $itemid, $text, %args) = @_;
286              
287 12   100     85 my $grogname = $args{'Grogname'} || ' ';
288 12         28 my $seq;
289              
290 12         41 my $item_was_full = $self->item_is_full;
291              
292 12         20 delete ${*$self}{'net_rgtp_item_is_full'};
  12         42  
293 12         21 delete ${*$self}{'net_rgtp_item_has_grown'};
  12         37  
294              
295 12         56 $self->command('DATA');
296 12         12152 $self->response;
297              
298 12 50       118831 die "No posting access" if $self->code eq '531';
299 12         222 $self->_expect_code('150');
300              
301 12         183 $text =~ s/\n\./\n\.\./g; # dot-doubling
302              
303 12         88 $self->datasend("$grogname\n");
304 12         5569 $self->datasend($text);
305 12         3330 $self->dataend;
306              
307 12 50       429728 return undef if $self->_malformed_posting;
308 12         230 $self->_expect_code('350');
309              
310 12 100 100     260 if ($itemid eq 'new' || $itemid eq 'continue') {
    50          
311              
312 2 50       71 my $subject = $args{'Subject'}
313             or die "Need a subject line";
314              
315 2 100       7 if ($itemid eq 'continue') {
316 1 50       7 die "We haven't reached the end of an item"
317             unless $item_was_full;
318              
319 1         12 $self->command('CONT', $subject);
320             } else {
321 1         7 $self->command('NEWI', $subject);
322             }
323              
324 2         725 $self->response;
325              
326 2 50       32252 return undef if $self->_malformed_posting;
327              
328 2 50       23 if ($self->code eq '122') {
329 0         0 $self->response;
330 0         0 $self->_expect_code('422');
331              
332 0         0 ${*$self}{'net_rgtp_item_has_grown'} = 1;
  0         0  
333            
334 0         0 return undef;
335             }
336              
337 2 50       25 if ($self->code eq '520') {
338 0         0 $@ = 'We haven\'t reached the end of an item';
339             }
340              
341 2         27 $self->_expect_code('120');
342              
343 2         23 $self->response;
344 2         93193 $self->_expect_code('220');
345 2         37 ($itemid) = $self->message =~ /^([A-Za-z][0-9]{7})/;
346             # seq is extracted below.
347              
348             } elsif ($itemid eq 'motd') {
349              
350 0         0 $@ = 'Not implemented';
351 0         0 return undef;
352              
353             } else {
354              
355 10 50       49 return undef unless _is_valid_itemid($itemid);
356              
357 10 100       43 if (defined $args{'Seq'}) {
358 2         9 my $quick = $self->quick_item($itemid);
359              
360 2 100       14 if ($quick->{'reply'} != $args{'Seq'}) {
361 1         2 $@ = 'Item has apparently grown';
362 1         3 ${*$self}{'net_rgtp_item_has_grown'} = 1;
  1         183  
363 1         13 return undef;
364             }
365             }
366              
367 9         53 $self->command('REPL', $itemid);
368 9         2602 $self->response;
369              
370 9 50       101469 return undef if $self->_malformed_posting;
371              
372 9 100       42 if ($self->code eq '421') {
373             # Item is full.
374              
375 1         11 ${*$self}{'net_rgtp_item_is_full'} = 1;
  1         40  
376 1         3 $@ = 'Item is full';
377 1         12 return undef;
378             }
379              
380 8 50       108 if ($self->code eq '122') {
381 0         0 $self->response;
382 0         0 $self->_expect_code('422');
383              
384 0         0 ${*$self}{'net_rgtp_item_has_grown'} = 1;
  0         0  
385 0         0 $@ = 'Item has gone into a continuation';
386 0         0 return undef;
387             }
388            
389 8         150 $self->_expect_code('220');
390              
391             # So, success!
392              
393             }
394              
395 10         239 ($seq) = $self->message =~ /([A-Fa-f0-9]{8}) /;
396              
397 10 100       189 if (wantarray) {
398 1         9 return ($itemid, hex($seq));
399             } else {
400 9         86 return $itemid;
401             }
402             }
403              
404             sub item_is_full {
405 25     25 1 1984 my ($self) = @_;
406              
407 25         35 return defined ${*$self}{'net_rgtp_item_is_full'};
  25         128  
408             }
409              
410             sub item_has_grown {
411 4     4 1 13 my ($self) = @_;
412              
413 4         8 return defined ${*$self}{'net_rgtp_item_has_grown'};
  4         21  
414             }
415              
416             ################################################################
417             # INTERNAL ROUTINES
418              
419             sub _read_item {
420 4     4   12 my $self = shift;
421 4         13 my %args = @_;
422 4         7 my %result = ();
423 4         11 my @responses = ();
424 4         8 my $current_response = ();
425 4         8 my ($seq, $timestamp);
426              
427 4         19 $self->response;
428 4 50       44076 die "No reading access" if $self->code eq '531';
429 4 50       136 return undef if $self->code eq '410';
430 4         58 $self->_expect_code('250');
431              
432 4         54 my $status = $self->getline;
433              
434 4 100       158910 if ($args{'motd'}) {
435 1         26 ($seq, $timestamp) =
436             $status =~ /^([0-9a-fA-F]{8}|\s{8}) ([0-9a-fA-F]{8})/;
437            
438 1 50       6 if (${*$self}{'net_rgtp_groggsbug'}) {
  1         11  
439             # They have it backwards!
440 1         8 $result{'seq'} = hex($timestamp);
441 1         7 $result{'timestamp'} = hex($seq);
442             } else {
443 0         0 $result{'seq'} = hex($seq);
444 0         0 $result{'timestamp'} = hex($timestamp);
445             }
446             } else {
447 3         38 my ($parent, $child, $edit, $reply) =
448             $status =~ /^([A-Za-z]\d{7}|\s{8}) ([A-Za-z]\d{7}|\s{8}) ([0-9a-fA-F]{8}|\s{8}) ([0-9a-fA-F]{8})/;
449            
450 3 100       21 $result{'parent'} = $parent if $parent ne ' ';
451 3 100       16 $result{'child' } = $child if $child ne ' ';
452 3 50       10 $result{'edit' } = hex($edit) if $edit ne ' ';
453 3         16 $result{'reply' } = hex($reply);
454             }
455              
456 4         10 for my $line (@{$self->read_until_dot}) {
  4         41  
457 445 100       22127 if (($seq, $timestamp) = $line =~ /^\^([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})/) {
458 15 100       46 push @responses, $current_response if $current_response;
459 15         72 $current_response = { seq=>hex($seq), timestamp=>hex($timestamp) };
460            
461             } else {
462 430         595 $line =~ s/^\^\^/\^/;
463 430         698 $line =~ s/^\.\./\./;
464 430         810 $current_response->{'text'} .= $line;
465             }
466             }
467              
468 4         48 $current_response->{'text'} =~ s/\n\n$/\n/;
469              
470 4         10 push @responses, $current_response;
471              
472 4 100       36 unless ($args{'no_parse_headers'}) {
473 3         8 for my $response (@responses) {
474              
475 15         60 $response->{'text'} =~ s/\n\n$/\n/;
476              
477 15         72 my $username;
478 15 100       118 if (($username) = $response->{'text'} =~ /^.* from (.*) at .*\n/) {
479            
480 14 100       44 if ($username =~ /\(.*\)$/) {
481 5         37 ($response->{'grogname'}, $response->{'poster'}) =
482             $username =~ /^(.*) \((.*)\)$/;
483             } else {
484 9         18 $response->{'poster'} = $username;
485 9 100       43 if ($response->{'text'} =~ /From (.*)\n/) {
486 1         7 $response->{'grogname'} = $1;
487             }
488             }
489            
490             }
491            
492 15 100       63 if ($response->{'text'} =~ /Subject: (.*)\n/) {
493 3         15 $result{'subject'} = $1;
494             }
495            
496 15         267 $response->{'text'} =~ s/^(.|\r|\n)*?\r?\n\r?\n//;
497             }
498             }
499              
500 4         17 $result{'posts'} = \@responses;
501              
502 4 100       17 if ($args{'motd'}) {
503 1         8 $result{'posts'}[0]->{'seq'} = delete $result{'seq'};
504 1         5 $result{'posts'}[0]->{'timestamp'} = delete $result{'timestamp'};
505             }
506              
507 4         137 \%result;
508             }
509              
510             sub _is_valid_itemid {
511 13 50   13   90 if (shift =~ /^[A-Za-z]\d{7}$/) {
512 13         55 return 1;
513             } else {
514 0         0 $@ = 'Invalid itemid';
515 0         0 return 0;
516             }
517             }
518              
519             sub _set_alvl {
520 6     6   17 my $self = shift;
521              
522 6 50 33     24 die "Expected status response"
523             if $self->code()<230 || $self->code()>233;
524              
525 6         159 ${*$self}{'net_rgtp_status'} = $self->code()-230;
  6         70  
526             }
527              
528             sub _expect_code {
529 45     45   290 my ($self, $expectation) = @_;
530              
531 45 50       160 if ($self->code ne $expectation) {
532 0         0 die "Low-level protocol error: expected $expectation and got ".$self->code;
533             }
534             }
535              
536             sub _malformed_posting {
537              
538 23     23   85 my $self = shift;
539              
540 23 50       127 if ($self->code eq '423') { $@ = 'Malformed text'; return 1; }
  0         0  
  0         0  
541 23 50       405 if ($self->code eq '424') { $@ = 'Malformed subject'; return 1; }
  0         0  
  0         0  
542 23 50       283 if ($self->code eq '425') { $@ = 'Malformed grogname'; return 1; }
  0         0  
  0         0  
543              
544 23         274 return 0;
545             }
546              
547             1;
548              
549             __END__