File Coverage

blib/lib/Mail/Karmasphere/Client.pm
Criterion Covered Total %
statement 121 180 67.2
branch 37 108 34.2
condition 1 3 33.3
subroutine 23 27 85.1
pod 4 5 80.0
total 186 323 57.5


line stmt bran cond sub pod time code
1             package Mail::Karmasphere::Client;
2              
3 5     5   4253 use strict;
  5         11  
  5         170  
4 5     5   24 use warnings;
  5         11  
  5         183  
5 5         507 use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS
6 5     5   32 %QUEUE @QUEUE $QUEUE);
  5         9  
7 5     5   27 use Exporter;
  5         9  
  5         214  
8 5     5   6897 use Data::Dumper;
  5         32832  
  5         487  
9 5     5   7179 use Convert::Bencode qw(bencode bdecode);
  5         9148  
  5         383  
10 5     5   5559 use IO::Socket::INET;
  5         129037  
  5         48  
11 5     5   10974 use Time::HiRes;
  5         10240  
  5         27  
12 5     5   5856 use IO::Select;
  5         8467  
  5         248  
13 5     5   32 use Socket;
  5         11  
  5         5716  
14             use constant {
15 5         768 IDT_IP4_ADDRESS => "ip4",
16             IDT_IP6_ADDRESS => "ip6",
17             IDT_DOMAIN_NAME => "domain",
18             IDT_EMAIL_ADDRESS => "email",
19              
20             IDT_IP4 => "ip4",
21             IDT_IP6 => "ip6",
22             IDT_DOMAIN => "domain",
23             IDT_EMAIL => "email",
24             IDT_URL => "url",
25 5     5   34 };
  5         14  
26             use constant {
27 5         650 AUTHENTIC => "a",
28             SMTP_CLIENT_IP => "smtp.client-ip",
29             SMTP_ENV_HELO => "smtp.env.helo",
30             SMTP_ENV_MAIL_FROM => "smtp.env.mail-from",
31             SMTP_ENV_RCPT_TO => "smtp.env.rcpt-to",
32             SMTP_HEADER_FROM_ADDRESS => "smtp.header.from.address",
33              
34             FL_FACTS => 1,
35             FL_DATA => 2,
36             FL_TRACE => 4,
37             FL_MODELTRACE => 8,
38 5     5   25 };
  5         8  
39             use constant {
40 5         5164 PROTO_TCP => 0+getprotobyname('tcp'),
41             PROTO_UDP => 0+getprotobyname('udp'),
42 5     5   32 };
  5         8  
43              
44             BEGIN {
45 5     5   77 @ISA = qw(Exporter);
46 5         12 $VERSION = "2.18";
47 5         20 @EXPORT_OK = qw(
48             IDT_IP4_ADDRESS IDT_IP6_ADDRESS
49             IDT_DOMAIN_NAME IDT_EMAIL_ADDRESS
50              
51             IDT_IP4 IDT_IP6
52             IDT_DOMAIN IDT_EMAIL
53             IDT_URL
54              
55             AUTHENTIC
56             SMTP_CLIENT_IP
57             SMTP_ENV_HELO SMTP_ENV_MAIL_FROM SMTP_ENV_RCPT_TO
58             SMTP_HEADER_FROM_ADDRESS
59              
60             FL_FACTS
61             FL_DATA
62             FL_TRACE
63             FL_MODELTRACE
64             );
65 5         24 %EXPORT_TAGS = (
66             'all' => \@EXPORT_OK,
67             'ALL' => \@EXPORT_OK,
68             );
69 5         16 %QUEUE = ();
70 5         10 @QUEUE = ();
71 5         98 $QUEUE = 100;
72             }
73              
74             # We can't use these until we set up the above variables.
75 5     5   3432 use Mail::Karmasphere::Query;
  5         32  
  5         222  
76 5     5   2876 use Mail::Karmasphere::Response;
  5         14  
  5         8572  
77              
78             sub new {
79 2     2 0 3486 my $class = shift;
80 2 50       16 my $self = ($#_ == 0) ? { %{ (shift) } } : { @_ };
  0         0  
81              
82 2 50 33     13 if ($self->{Debug} and ref($self->{Debug}) ne 'CODE') {
83 0     0   0 $self->{Debug} = sub { print STDERR Dumper(@_); };
  0         0  
84             }
85 2 50       14 $self->{Debug}->('new', $self) if $self->{Debug};
86              
87 2 50       8 unless ($self->{Socket}) {
88 2 50       9 $self->{Proto} = 'udp'
89             unless defined $self->{Proto};
90 2 50       9 $self->{PeerAddr} = $self->{PeerHost}
91             unless defined $self->{PeerAddr};
92 2 50       7 $self->{PeerAddr} = 'query.karmasphere.com'
93             unless defined $self->{PeerAddr};
94 2 50       7 $self->{PeerPort} = 8666
95             unless $self->{Port};
96 2         7 _connect($self);
97             }
98              
99 2         18 return bless $self, $class;
100             }
101              
102             sub _connect {
103 3     3   6 my $self = shift;
104 3 50       9 $self->{Debug}->('connect') if $self->{Debug};
105 3         29 $self->{Socket} = new IO::Socket::INET(
106             Proto => $self->{Proto},
107             PeerAddr => $self->{PeerAddr},
108             PeerPort => $self->{PeerPort},
109             ReuseAddr => 1,
110             );
111 3 100       127200076 unless (defined $self->{Socket}) {
112 1         7 delete $self->{Socket};
113 1         10 my @args = map { "$_=" . $self->{$_} } keys %$self;
  7         1897  
114 1         365 die "Failed to create socket: $! (@args)";
115             }
116             }
117              
118             sub query {
119 0     0 1 0 my $self = shift;
120 0         0 return $self->ask(new Mail::Karmasphere::Query(@_));
121             }
122              
123             sub _previous_socket {
124 4     4   6 my $self = shift;
125 4 50       135 return undef unless exists $self->{PreviousTime};
126 0 0       0 if ($self->{PreviousTime} + 10 > time()) {
127 0 0       0 $self->{Debug}->('previous') if $self->{Debug};
128 0         0 return $self->{PreviousSocket}
129             }
130 0         0 delete $self->{PreviousSocket};
131 0         0 delete $self->{PreviousTime};
132 0         0 return undef;
133             }
134              
135             sub _is_tcp {
136 0     0   0 my ($self, $socket) = @_;
137 0         0 return $socket->protocol == PROTO_TCP;
138             }
139              
140             sub _send_real {
141 10     10   20 my ($self, $data) = @_;
142              
143 10         14 my $socket = $self->{Socket};
144              
145 10 50       51 if ($socket->protocol == PROTO_UDP) {
146 10 100       134 if (length $data > 1024) { # Server's UDP_MAX
147 1 50       5 $self->{Debug}->('fallback') if $self->{Debug};
148 1         3 $self->{PreviousSocket} = $self->{Socket};
149 1         3 $self->{PreviousTime} = time();
150 1         2 $self->{Proto} = 'tcp';
151             # XXX This loses the old socket and any queries
152             # sent thereon.
153 1         4 $self->_connect();
154 0         0 $socket = $self->{Socket};
155             }
156             }
157             # This can NOT be an else as we clobber the variable above.
158 9 50       26 if ($socket->protocol == PROTO_TCP) {
159 0 0       0 $self->{Debug}->('tcp prefix') if $self->{Debug};
160 0         0 $data = pack("N", length($data)) . $data;
161 0 0       0 $self->{Debug}->('send_real', $data) if $self->{Debug};
162             }
163 9 50       123 $socket->send($data)
164             or die "Failed to send to socket: $!";
165             }
166              
167             sub send {
168 10     10 1 31 my ($self, $query) = @_;
169              
170 10 50       65 die "Not blessed reference: $query"
171             unless ref($query) =~ /[a-z]/;
172 10 50       60 die "Not a query: $query"
173             unless $query->isa('Mail::Karmasphere::Query');
174              
175 10 50       42 $self->{Debug}->('send_query', $query) if $self->{Debug};
176              
177 10         35 my $id = $query->id;
178              
179 10         31 my $packet = {
180             _ => $id,
181             };
182 10 50       64 $packet->{i} = $query->identities if $query->has_identities;
183 10 50       27 $packet->{s} = $query->composites if $query->has_composites;
184 10 100       32 $packet->{f} = $query->feeds if $query->has_feeds;
185 10 50       30 $packet->{c} = $query->combiners if $query->has_combiners;
186 10 50       37 $packet->{fl} = $query->flags if $query->has_flags;
187 10 50       27 if (defined $self->{Principal}) {
188 0 0       0 my $creds = defined $self->{Credentials} ?$self->{Credentials} : '';
189 0         0 $packet->{a} = [ $self->{Principal}, $creds ];
190             }
191 10 50       22 $self->{Debug}->('send_packet', $packet) if $self->{Debug};
192              
193 10         38 my $data = bencode($packet);
194 10 50       980 $self->{Debug}->('send_data', $data) if $self->{Debug};
195              
196 10         27 $self->_send_real($data);
197              
198 9         1139 return $id;
199             }
200              
201             sub _recv_real {
202 0     0   0 my ($self, $socket) = @_;
203              
204 0         0 my $data;
205 0 0       0 if ($socket->protocol == PROTO_TCP) {
206 0 0       0 $socket->read($data, 4)
207             or die "Failed to receive length from socket: $!";
208 0         0 my $length = unpack("N", $data);
209 0         0 $data = '';
210 0         0 while ($length > 0) {
211 0         0 my $block;
212 0 0       0 my $bytes = $socket->read($block, $length)
213             or die "Failed to receive data from socket: $!";
214 0         0 $data .= $block;
215 0         0 $length -= $bytes;
216             }
217 0 0       0 $self->{Debug}->('recv_data', $data) if $self->{Debug};
218             }
219             else {
220 0 0       0 $socket->recv($data, 8192)
221             or die "Failed to receive from socket: $!";
222 0 0       0 $self->{Debug}->('recv_data', $data) if $self->{Debug};
223             }
224 0         0 my $packet = bdecode($data);
225 0 0       0 die $packet unless ref($packet) eq 'HASH';
226              
227 0         0 my $response = new Mail::Karmasphere::Response($packet);
228 0 0       0 $self->{Debug}->('recv_response', $response) if $self->{Debug};
229 0         0 return $response;
230             }
231              
232             sub recv {
233 4     4 1 4034 my ($self, $query, $timeout) = @_;
234              
235 4 50       30 my $id = ref($query) ? $query->id : $query;
236 4 50       22 if (defined($id)) {
237 0 0       0 if ($QUEUE{$id}) {
238 0 0       0 $self->{Debug}->('recv_find', $id, $QUEUE{$id})
239             if $self->{Debug};
240 0         0 @QUEUE = grep { $_ ne $id } @QUEUE;
  0         0  
241 0         0 return delete $QUEUE{$id};
242             }
243             }
244             else {
245 4 50       20 if (@QUEUE) {
246 0         0 $id = shift @QUEUE;
247 0         0 return delete $QUEUE{$id};
248             }
249             }
250              
251 4 50       255 $timeout = 10 unless defined $timeout;
252 4         12 my $finish = time() + $timeout;
253 4         28 my $select = new IO::Select();
254 4         68 $select->add($self->{Socket});
255 4         189 my $prev = $self->_previous_socket;
256 4 50       12 $select->add($prev) if $prev;
257 4         11 while ($timeout > 0) {
258 4         19 my @ready = $select->can_read($timeout);
259              
260 4 50       40036765 if (@ready) {
261 0         0 my $response = $self->_recv_real($ready[0]);
262 0 0       0 $response->{query} = $query if ref $query;
263 0 0       0 return $response unless defined $id;
264 0 0       0 return $response if $response->id eq $id;
265              
266 0         0 my $rid = $response->id;
267 0         0 push(@QUEUE, $rid);
268 0         0 $QUEUE{$rid} = $response;
269 0 0       0 if (@QUEUE > $QUEUE) {
270 0         0 my $oid = shift @QUEUE;
271 0         0 delete $QUEUE{$oid};
272             }
273             }
274              
275 4         31 $timeout = $finish - time();
276             }
277              
278 4         897 print STDERR "Failed to receive from socket: $!\n";
279 4         125 return undef;
280             }
281              
282             sub ask {
283 1     1 1 8 my ($self, $query, $timeout) = @_;
284 1 50       5 $timeout = 5 unless defined $timeout;
285 1         3 for (0..2) {
286 1         4 my $id = $self->send($query);
287 0           my $response = $self->recv($query, $timeout);
288             # $response->{query} = $query;
289 0 0         return $response if $response;
290 0           $timeout += $timeout;
291             }
292 0           return undef;
293             }
294              
295             =head1 NAME
296              
297             Mail::Karmasphere::Client - Client for Karmasphere Reputation Server
298              
299             =head1 SYNOPSIS
300              
301             use Mail::Karmasphere::Client qw(:all);
302            
303             my $client = new Mail::Karmasphere::Client(
304             PeerAddr => 'query.karmasphere.com',
305             PeerPort => 8666,
306             Principal => "my_assigned_query_username",
307             Credentials => "my_assigned_query_password",
308             # see http://my.karmasphere.com/devzone/client/configuration#credentials
309             # quickstart: use temporary credentials for "generic perl".
310             # recommended: use permanent credentials -- register for an account.
311             );
312            
313             my $query = new Mail::Karmasphere::Query();
314             $query->identity('127.0.0.2', IDT_IP4);
315             $query->composite('karmasphere.email-sender');
316             my $response = $client->ask($query, 6);
317             print $response->as_string;
318            
319             my $id = $client->send($query);
320             my $response = $client->recv($query, 12);
321             my $response = $client->recv($id, 12);
322            
323             my $response = $client->query(
324             Identities => [ ... ]
325             Composite => 'karmasphere.email-sender',
326             );
327              
328             =head1 DESCRIPTION
329              
330             The Perl Karma Client API consists of three objects: The Query, the
331             Response, and the Client. The user constructs a Query and passes it
332             to a Client, which returns a Response.
333              
334             =head1 CONSTRUCTOR
335              
336             The class method new(...) constructs a new Client object. All arguments
337             are optional. The following parameters are recognised as arguments
338             to new():
339              
340             =over 4
341              
342             =item PeerAddr
343              
344             The IP address or hostname to contact. See L. The
345             default is 'query.karmasphere.com'.
346              
347             =item PeerPort
348              
349             The TCP or UDP to contact. See L. The default
350             is 8666.
351              
352             =item Proto
353              
354             Either 'udp' or 'tcp'. The default is 'udp' because it is faster.
355              
356             =item Principal
357              
358             =item Credentials
359              
360             A username and password are required to authenticate client
361             connections. They are assigned by Karmasphere. See
362             http://my.karmasphere.com/devzone/client/configuration#credentials
363              
364             "Principal" corresponds to "username", and "Credentials"
365             corresponds to "password". Note that these are not the same
366             username and password you use to sign in to the website.
367              
368             =item Debug
369              
370             Either a true value for debugging to stderr, or a custom debug handler.
371             The custom handler will be called with N arguments, the first of which
372             is a string 'debug context'. The custom handler may choose to ignore
373             messages from certain contexts.
374              
375             =back
376              
377             =head1 METHODS
378              
379             =over 4
380              
381             =item $response = $client->ask($query, $timeout)
382              
383             Returns a L to a
384             L. The core of this method is equivalent to
385              
386             $client->recv($client->send($query), $timeout)
387              
388             The method retries up to 3 times, doubling the timeout each time. If
389             the application requires more control over retries or backoff, it
390             should use send() and recv() individually. $timeout is optional.
391              
392             =item $id = $client->send($query)
393              
394             Sends a L to the server, and returns the
395             id of the query, which may be passed to recv(). Note that any query
396             longer than 64KB will be rejected by the server with a message advising
397             that the maximum message length has been exceeded.
398              
399             =item $response = $client->recv($id, $timeout)
400              
401             Returns a L to the query with id $id,
402             assuming that the query has already been sent using send(). If no
403             matching response is read before the timeout, undef is returned.
404              
405             =item $response = $client->query(...)
406              
407             A convenience method, equivalent to
408              
409             $client->ask(new Mail::Karmasphere::Query(...));
410              
411             See L for more details.
412              
413             =back
414              
415             =head1 EXPORTS
416              
417             =over 4
418              
419             =item IDT_IP4 IDT_IP6 IDT_DOMAIN IDT_EMAIL IDT_URL
420              
421             Identity type constants.
422              
423             =item AUTHENTIC SMTP_CLIENT_IP SMTP_ENV_HELO SMTP_ENV_MAIL_FROM SMTP_ENV_RCPT_TO SMTP_HEADER_FROM_ADDRESS
424              
425             Identity tags, indicating the context of an identity to the server.
426              
427             =item FL_FACTS
428              
429             A flag indicating that all facts must be returned explicitly in the
430             Response.
431              
432             =back
433              
434             =head1 NOTES ON THE IMPLEMENTATION
435              
436             The server will discard any packet in TCP mode which exceeds
437             64K. Although the packet length field is 4 bytes, it is relatively
438             common to get non-Karmasphere clients connecting to the port.
439             Therefore the server checks that the top two bytes are \0 before
440             accepting the packet. This saves everybody a headache.
441              
442             Some flags, notably those which generate large response packets,
443             are totally ignored for UDP queries, even in the case that they would
444             not generate a large response. This also saves many headaches.
445              
446             =head1 BUGS
447              
448             UDP retries are not yet implemented.
449              
450             =head1 SEE ALSO
451              
452             L,
453             L,
454             http://www.karmasphere.com/,
455             http://my.karmasphere.com/devzone/client/configuration,
456             L
457              
458             =head1 COPYRIGHT
459              
460             Copyright (c) 2005-2006 Shevek, Karmasphere. All rights reserved.
461              
462             This program is free software; you can redistribute it and/or modify
463             it under the same terms as Perl itself.
464              
465             =cut
466              
467             1;