ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MP/MP/Transport.pm
(Generate patch)

Comparing AnyEvent-MP/MP/Transport.pm (file contents):
Revision 1.5 by root, Sun Aug 2 14:44:37 2009 UTC vs.
Revision 1.26 by root, Sat Aug 8 21:56:29 2009 UTC

1=head1 NAME 1=head1 NAME
2 2
3AnyEvent::MP::Transport - actual transport protocol 3AnyEvent::MP::Transport - actual transport protocol handler
4 4
5=head1 SYNOPSIS 5=head1 SYNOPSIS
6 6
7 use AnyEvent::MP::Transport; 7 use AnyEvent::MP::Transport;
8 8
9=head1 DESCRIPTION 9=head1 DESCRIPTION
10 10
11This is the superclass for MP transports, most of which is considered an 11This implements the actual transport protocol for MP (it represents a
12implementation detail. 12single link), most of which is considered an implementation detail.
13 13
14Future versions might document the actual protocol. 14See the "PROTOCOL" section below if you want to write another client for
15this protocol.
15 16
16=head1 FUNCTIONS/METHODS 17=head1 FUNCTIONS/METHODS
17 18
18=over 4 19=over 4
19 20
26use Scalar::Util; 27use Scalar::Util;
27use MIME::Base64 (); 28use MIME::Base64 ();
28use Storable (); 29use Storable ();
29use JSON::XS (); 30use JSON::XS ();
30 31
32use Digest::MD6 ();
33use Digest::HMAC_MD6 ();
34
31use AE (); 35use AE ();
32use AnyEvent::Socket (); 36use AnyEvent::Socket ();
33use AnyEvent::Handle (); 37use AnyEvent::Handle ();
34 38
35use AnyEvent::MP::Util ();
36
37use base Exporter::; 39use base Exporter::;
38 40
39our $VERSION = '0.0'; 41our $VERSION = '0.0';
40our $PROTOCOL_VERSION = 0; 42our $PROTOCOL_VERSION = 0;
41 43
44Creates a listener on the given host/port using 46Creates a listener on the given host/port using
45C<AnyEvent::Socket::tcp_server>. 47C<AnyEvent::Socket::tcp_server>.
46 48
47See C<new>, below, for constructor arguments. 49See C<new>, below, for constructor arguments.
48 50
49Defaults for peerhost, peerport, fh and tls are provided. 51Defaults for peerhost, peerport and fh are provided.
50 52
51=cut 53=cut
52 54
53sub mp_server($$@) { 55sub mp_server($$@) {
54 my $cb = pop; 56 my $cb = pop;
59 61
60 $cb->(new AnyEvent::MP::Transport 62 $cb->(new AnyEvent::MP::Transport
61 fh => $fh, 63 fh => $fh,
62 peerhost => $host, 64 peerhost => $host,
63 peerport => $port, 65 peerport => $port,
64 tls => "accept",
65 @args, 66 @args,
66 ); 67 );
67 } 68 }
68} 69}
69 70
83 $cb->(new AnyEvent::MP::Transport 84 $cb->(new AnyEvent::MP::Transport
84 fh => $fh, 85 fh => $fh,
85 peername => $host, 86 peername => $host,
86 peerhost => $nhost, 87 peerhost => $nhost,
87 peerport => $nport, 88 peerport => $nport,
88 tls => "accept",
89 @args, 89 @args,
90 ); 90 );
91 } 91 }
92} 92}
93 93
106 on_eof => sub { clean-close-callback }, 106 on_eof => sub { clean-close-callback },
107 on_connect => sub { successful-connect-callback }, 107 on_connect => sub { successful-connect-callback },
108 greeting => { key => value }, 108 greeting => { key => value },
109 109
110 # tls support 110 # tls support
111 tls => "accept|connect",
112 tls_ctx => AnyEvent::TLS, 111 tls_ctx => AnyEvent::TLS,
113 peername => $peername, # for verification 112 peername => $peername, # for verification
114 ; 113 ;
115 114
116=cut 115=cut
117 116
118our @FRAMING_WANT = qw(json storable);#d##TODO# 117our @FRAMINGS = qw(json storable); # the framing types we accept and send, in order of preference
118our @AUTH_SND = qw(hmac_md6_64_256); # auth types we send
119our @AUTH_RCV = (@AUTH_SND, qw(cleartext)); # auth types we accept
120
121#AnyEvent::Handle::register_write_type mp_record => sub {
122#};
119 123
120sub new { 124sub new {
121 my ($class, %arg) = @_; 125 my ($class, %arg) = @_;
122 126
123 my $self = bless \%arg, $class; 127 my $self = bless \%arg, $class;
125 $self->{queue} = []; 129 $self->{queue} = [];
126 130
127 { 131 {
128 Scalar::Util::weaken (my $self = $self); 132 Scalar::Util::weaken (my $self = $self);
129 133
130 if (exists $arg{connect}) {
131 $arg{tls} ||= "connect";
132 $arg{tls_ctx} ||= { sslv2 => 0, sslv3 => 0, tlsv1 => 1, verify => 1 };
133 }
134
135 $arg{secret} = AnyEvent::MP::Base::default_secret () 134 $arg{secret} = AnyEvent::MP::Base::default_secret ()
136 unless exists $arg{secret}; 135 unless exists $arg{secret};
137 136
137 $arg{timeout} = 3#d#
138 unless exists $arg{timeout};
139
140 my $secret = $arg{secret};
141
142 if ($secret =~ /-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/s) {
143 # assume TLS mode
144 $arg{tls_ctx} = {
145 sslv2 => 0,
146 sslv3 => 0,
147 tlsv1 => 1,
148 verify => 1,
149 cert => $secret,
150 ca_cert => $secret,
151 verify_require_client_cert => 1,
152 };
153 }
154
138 $self->{hdl} = new AnyEvent::Handle 155 $self->{hdl} = new AnyEvent::Handle
139 fh => delete $arg{fh}, 156 fh => delete $arg{fh},
140 rbuf_max => 64 * 1024,
141 autocork => 1, 157 autocork => 1,
142 no_delay => 1, 158 no_delay => 1,
143 on_error => sub { 159 on_error => sub {
144 $self->error ($_[2]); 160 $self->error ($_[2]);
145 }, 161 },
162 timeout => $AnyEvent::MP::Base::CONNECT_TIMEOUT,
146 peername => delete $arg{peername}, 163 peername => delete $arg{peername},
147 ; 164 ;
148 165
149 my $secret = $arg{secret};
150 my $greeting_kv = $self->{greeting} ||= {}; 166 my $greeting_kv = $self->{greeting} ||= {};
167
168 $self->{local_node} = $AnyEvent::MP::Base::NODE;
169
151 $greeting_kv->{"tls1.0"} ||= $arg{tls} 170 $greeting_kv->{"tls"} = "1.0" if $arg{tls_ctx};
152 if exists $arg{tls} && $arg{tls_ctx};
153 $greeting_kv->{provider} = "AE-$VERSION"; 171 $greeting_kv->{provider} = "AE-$VERSION";
172 $greeting_kv->{peeraddr} = AnyEvent::Socket::format_hostport $self->{peerhost}, $self->{peerport};
173 $greeting_kv->{timeout} = $arg{timeout};
154 174
155 # send greeting 175 # send greeting
156 my $lgreeting = "aemp;$PROTOCOL_VERSION;$PROTOCOL_VERSION" # version, min 176 my $lgreeting1 = "aemp;$PROTOCOL_VERSION"
157 . ";$AnyEvent::MP::Base::UNIQ" 177 . ";$self->{local_node}"
158 . ";$AnyEvent::MP::Base::NODE" 178 . ";" . (join ",", @AUTH_RCV)
159 . ";" . (MIME::Base64::encode_base64 AnyEvent::MP::Base::nonce (33), "") 179 . ";" . (join ",", @FRAMINGS)
160 . ";hmac_md6_64_256" # hardcoded atm.
161 . ";json" # hardcoded atm.
162 . ";$self->{peerhost};$self->{peerport}"
163 . (join "", map ";$_=$greeting_kv->{$_}", keys %$greeting_kv); 180 . (join "", map ";$_=$greeting_kv->{$_}", keys %$greeting_kv);
164 181
182 my $lgreeting2 = MIME::Base64::encode_base64 AnyEvent::MP::Base::nonce (33), "";
183
165 $self->{hdl}->push_write ("$lgreeting\012"); 184 $self->{hdl}->push_write ("$lgreeting1\012$lgreeting2\012");
166 185
167 # expect greeting 186 # expect greeting
187 $self->{hdl}->rbuf_max (4 * 1024);
168 $self->{hdl}->push_read (line => sub { 188 $self->{hdl}->push_read (line => sub {
169 my $rgreeting = $_[1]; 189 my $rgreeting1 = $_[1];
170 190
171 my ($aemp, $version, $version_min, $uniq, $rnode, undef, $auth, $framing, $peerport, $peerhost, @kv) = split /;/, $rgreeting; 191 my ($aemp, $version, $rnode, $auths, $framings, @kv) = split /;/, $rgreeting1;
172 192
173 if ($aemp ne "aemp") { 193 if ($aemp ne "aemp") {
174 return $self->error ("unparsable greeting"); 194 return $self->error ("unparsable greeting");
175 } elsif ($version_min > $PROTOCOL_VERSION) { 195 } elsif ($version != $PROTOCOL_VERSION) {
176 return $self->error ("version mismatch (we: $PROTOCOL_VERSION, they: $version_min .. $version)"); 196 return $self->error ("version mismatch (we: $PROTOCOL_VERSION, they: $version)");
177 } elsif ($auth ne "hmac_md6_64_256") {
178 return $self->error ("unsupported auth method ($auth)");
179 } elsif ($framing ne "json") {
180 return $self->error ("unsupported framing method ($auth)");
181 } 197 }
182 198
183 $self->{remote_uniq} = $uniq; 199 my $s_auth;
200 for my $auth_ (split /,/, $auths) {
201 if (grep $auth_ eq $_, @AUTH_SND) {
202 $s_auth = $auth_;
203 last;
204 }
205 }
206
207 defined $s_auth
208 or return $self->error ("$auths: no common auth type supported");
209
210 die unless $s_auth eq "hmac_md6_64_256"; # hardcoded atm.
211
212 my $s_framing;
213 for my $framing_ (split /,/, $framings) {
214 if (grep $framing_ eq $_, @FRAMINGS) {
215 $s_framing = $framing_;
216 last;
217 }
218 }
219
220 defined $s_framing
221 or return $self->error ("$framings: no common framing method supported");
222
184 $self->{remote_node} = $rnode; 223 $self->{remote_node} = $rnode;
185 224
186 $self->{remote_greeting} = { 225 $self->{remote_greeting} = {
187 map /^([^=]+)(?:=(.*))?/ ? ($1 => $2) : (), 226 map /^([^=]+)(?:=(.*))?/ ? ($1 => $2) : (),
188 @kv 227 @kv
189 }; 228 };
190 229
191 if (exists $self->{tls} and $self->{tls_ctx} and exists $self->{remote_greeting}{"tls1.0"}) { 230 # read nonce
231 $self->{hdl}->push_read (line => sub {
232 my $rgreeting2 = $_[1];
233
234 "$lgreeting1\012$lgreeting2" ne "$rgreeting1\012$rgreeting2" # echo attack?
235 or return $self->error ("authentication error, echo attack?");
236
237 my $key = Digest::MD6::md6 $secret;
238 my $lauth;
239
192 if ($self->{tls} ne $self->{remote_greeting}{"tls1.0"}) { 240 if ($self->{tls_ctx} and 1 == int $self->{remote_greeting}{tls}) {
193 return $self->error ("TLS server/client mismatch"); 241 $self->{tls} = $lgreeting2 lt $rgreeting2 ? "connect" : "accept";
242 $self->{hdl}->starttls ($self->{tls}, $self->{tls_ctx});
243 $s_auth = "tls";
244 $lauth = "";
245 } else {
246 # we currently only support hmac_md6_64_256
247 $lauth = Digest::HMAC_MD6::hmac_md6_hex $key, "$lgreeting1\012$lgreeting2\012$rgreeting1\012$rgreeting2\012", 64, 256;
194 } 248 }
195 $self->{hdl}->starttls ($self->{tls}, $self->{tls_ctx});
196 }
197 249
198 # auth
199 require Digest::MD6;
200 require Digest::HMAC_MD6;
201
202 my $key = Digest::MD6::md6_hex ($secret);
203 my $lauth = Digest::HMAC_MD6::hmac_md6_base64 ($key, "$lgreeting\012$rgreeting", 64, 256);
204 my $rauth = Digest::HMAC_MD6::hmac_md6_base64 ($key, "$rgreeting\012$lgreeting", 64, 256);
205
206 $lauth ne $rauth # echo attack?
207 or return $self->error ("authentication error");
208
209 $self->{hdl}->push_write ("$auth;$lauth;$framing\012"); 250 $self->{hdl}->push_write ("$s_auth;$lauth;$s_framing\012");
210 251
211 $self->{hdl}->rbuf_max (64); # enough for 44 reply bytes or so 252 # read the authentication response
212 $self->{hdl}->push_read (line => sub { 253 $self->{hdl}->push_read (line => sub {
213 my ($hdl, $rline) = @_; 254 my ($hdl, $rline) = @_;
214 255
215 my ($auth_method, $rauth2, $r_framing) = split /;/, $rline; 256 my ($auth_method, $rauth2, $r_framing) = split /;/, $rline;
216 257
258 my $rauth =
259 $auth_method eq "hmac_md6_64_256" ? Digest::HMAC_MD6::hmac_md6_hex $key, "$rgreeting1\012$rgreeting2\012$lgreeting1\012$lgreeting2\012", 64, 256
260 : $auth_method eq "cleartext" ? unpack "H*", $secret
261 : $auth_method eq "tls" ? ($self->{tls} ? "" : "\012\012") # \012\012 never matches
262 : return $self->error ("$auth_method: fatal, selected unsupported auth method");
263
217 if ($rauth2 ne $rauth) { 264 if ($rauth2 ne $rauth) {
218 return $self->error ("authentication failure/shared secret mismatch"); 265 return $self->error ("authentication failure/shared secret mismatch");
219 } 266 }
220 267
221 $self->{s_framing} = "json";#d# 268 $self->{s_framing} = $s_framing;
222 269
223 $hdl->rbuf_max (undef); 270 $hdl->rbuf_max (undef);
224 my $queue = delete $self->{queue}; # we are connected 271 my $queue = delete $self->{queue}; # we are connected
225 272
273 $self->{hdl}->timeout ($self->{remote_greeting}{timeout});
274
226 $self->connected; 275 $self->connected;
227 276
228 $hdl->push_write ($self->{s_framing} => $_) 277 my $src_node = $self->{node};
278
279 $self->send ($_)
229 for @$queue; 280 for @$queue;
230 281
231 my $rmsg; $rmsg = sub { 282 my $rmsg; $rmsg = sub {
232 $_[0]->push_read ($r_framing => $rmsg); 283 $_[0]->push_read ($r_framing => $rmsg);
233 284
285 local $AnyEvent::MP::Base::SRCNODE = $src_node;
234 AnyEvent::MP::Base::_inject ($_[1]); 286 AnyEvent::MP::Base::_inject (@{ $_[1] });
235 }; 287 };
236 $hdl->push_read ($r_framing => $rmsg); 288 $hdl->push_read ($r_framing => $rmsg);
289 });
237 }); 290 });
238 }); 291 });
239 } 292 }
240 293
241 $self 294 $self
243 296
244sub error { 297sub error {
245 my ($self, $msg) = @_; 298 my ($self, $msg) = @_;
246 299
247 if ($self->{node} && $self->{node}{transport} == $self) { 300 if ($self->{node} && $self->{node}{transport} == $self) {
301 #TODO: store error, but do not instantly fail
302 $self->{node}->fail (transport_error => $self->{node}{noderef}, $msg);
248 $self->{node}->clr_transport; 303 $self->{node}->clr_transport;
249 } 304 }
250# $self->{on_error}($self, $msg); 305 $AnyEvent::MP::Base::WARN->("$self->{peerhost}:$self->{peerport}: $msg");
251 $self->destroy; 306 $self->destroy;
252} 307}
253 308
254sub connected { 309sub connected {
255 my ($self) = @_; 310 my ($self) = @_;
311
312 if (ref $AnyEvent::MP::Base::SLAVE) {
313 # first connect with a master node
314 my $via = $self->{remote_node};
315 $via =~ s/,/!/g;
316 $AnyEvent::MP::Base::NODE .= "\@$via";
317 $AnyEvent::MP::Base::NODE{$AnyEvent::MP::Base::NODE} = $AnyEvent::MP::Base::NODE{""};
318 $AnyEvent::MP::Base::SLAVE->();
319 }
320
321 if ($self->{local_node} ne $AnyEvent::MP::Base::NODE) {
322 # node changed its name since first greeting
323 $self->send (["", iam => $AnyEvent::MP::Base::NODE]);
324 }
256 325
257 my $node = AnyEvent::MP::Base::add_node ($self->{remote_node}); 326 my $node = AnyEvent::MP::Base::add_node ($self->{remote_node});
258 Scalar::Util::weaken ($self->{node} = $node); 327 Scalar::Util::weaken ($self->{node} = $node);
259 $node->set_transport ($self); 328 $node->set_transport ($self);
260} 329}
276 $self->destroy; 345 $self->destroy;
277} 346}
278 347
279=back 348=back
280 349
350=head1 PROTOCOL
351
352The protocol is relatively simple, and consists of three phases which are
353symmetrical for both sides: greeting (followed by optionally switching to
354TLS mode), authentication and packet exchange.
355
356the protocol is designed to allow both full-text and binary streams.
357
358The greeting consists of two text lines that are ended by either an ASCII
359CR LF pair, or a single ASCII LF (recommended).
360
361=head2 GREETING
362
363All the lines until after authentication must not exceed 4kb in length,
364including delimiter. Afterwards there is no limit on the packet size that
365can be received.
366
367=head3 First Greeting Line
368
369Example:
370
371 aemp;0;fec.4a7720fc;127.0.0.1:1235,[::1]:1235;hmac_md6_64_256;json,storable;provider=AE-0.0
372
373The first line contains strings separated (not ended) by C<;>
374characters. The first even ixtrings are fixed by the protocol, the
375remaining strings are C<KEY=VALUE> pairs. None of them may contain C<;>
376characters themselves.
377
378The fixed strings are:
379
380=over 4
381
382=item protocol identification
383
384The constant C<aemp> to identify the protocol.
385
386=item protocol version
387
388The protocol version supported by this end, currently C<0>. If the
389versions don't match then no communication is possible. Minor extensions
390are supposed to be handled through additional key-value pairs.
391
392=item the node endpoint descriptors
393
394for public nodes, this is a comma-separated list of protocol endpoints,
395i.e., the noderef. For slave nodes, this is a unique identifier of the
396form C<slave/nonce>.
397
398=item the acceptable authentication methods
399
400A comma-separated list of authentication methods supported by the
401node. Note that AnyEvent::MP supports a C<hex_secret> authentication
402method that accepts a cleartext password (hex-encoded), but will not use
403this auth method itself.
404
405The receiving side should choose the first auth method it supports.
406
407=item the acceptable framing formats
408
409A comma-separated list of packet encoding/framign formats understood. The
410receiving side should choose the first framing format it supports for
411sending packets (which might be different from the format it has to accept).
412
413=back
414
415The remaining arguments are C<KEY=VALUE> pairs. The following key-value
416pairs are known at this time:
417
418=over 4
419
420=item provider=<module-version>
421
422The software provider for this implementation. For AnyEvent::MP, this is
423C<AE-0.0> or whatever version it currently is at.
424
425=item peeraddr=<host>:<port>
426
427The peer address (socket address of the other side) as seen locally, in the same format
428as noderef endpoints.
429
430=item tls=<major>.<minor>
431
432Indicates that the other side supports TLS (version should be 1.0) and
433wishes to do a TLS handshake.
434
435=item timeout=<seconds>
436
437The amount of time after which this node should be detected as dead unless
438some data has been received. The node is responsible to send traffic
439reasonably more often than this interval (such as every timeout minus five
440seconds).
441
442=back
443
444=head3 Second Greeting Line
445
446After this greeting line there will be a second line containing a
447cryptographic nonce, i.e. random data of high quality. To keep the
448protocol text-only, these are usually 32 base64-encoded octets, but
449it could be anything that doesn't contain any ASCII CR or ASCII LF
450characters.
451
452I<< The two nonces B<must> be different, and an aemp implementation
453B<must> check and fail when they are identical >>.
454
455Example of a nonce line:
456
457 p/I122ql7kJR8lumW3lXlXCeBnyDAvz8NQo3x5IFowE4
458
459=head2 TLS handshake
460
461I<< If, after the handshake, both sides indicate interest in TLS, then the
462connection B<must> use TLS, or fail. >>
463
464Both sides compare their nonces, and the side who sent the lower nonce
465value ("string" comparison on the raw octet values) becomes the client,
466and the one with the higher nonce the server.
467
468=head2 AUTHENTICATION PHASE
469
470After the greeting is received (and the optional TLS handshake),
471the authentication phase begins, which consists of sending a single
472C<;>-separated line with three fixed strings and any number of
473C<KEY=VALUE> pairs.
474
475The three fixed strings are:
476
477=over 4
478
479=item the authentication method chosen
480
481This must be one of the methods offered by the other side in the greeting.
482
483The currently supported authentication methods are:
484
485=over 4
486
487=item cleartext
488
489This is simply the shared secret, lowercase-hex-encoded. This method is of
490course very insecure, unless TLS is used, which is why this module will
491accept, but not generate, cleartext auth replies.
492
493=item hmac_md6_64_256
494
495This method uses an MD6 HMAC with 64 bit blocksize and 256 bit hash. First, the shared secret
496is hashed with MD6:
497
498 key = MD6 (secret)
499
500This secret is then used to generate the "local auth reply", by taking
501the two local greeting lines and the two remote greeting lines (without
502line endings), appending \012 to all of them, concatenating them and
503calculating the MD6 HMAC with the key.
504
505 lauth = HMAC_MD6 key, "lgreeting1\012lgreeting2\012rgreeting1\012rgreeting2\012"
506
507This authentication token is then lowercase-hex-encoded and sent to the
508other side.
509
510Then the remote auth reply is generated using the same method, but local
511and remote greeting lines swapped:
512
513 rauth = HMAC_MD6 key, "rgreeting1\012rgreeting2\012lgreeting1\012lgreeting2\012"
514
515This is the token that is expected from the other side.
516
517=item tls
518
519This type is only valid iff TLS was enabled and the TLS handshake
520was successful. It has no authentication data, as the server/client
521certificate was successfully verified.
522
523Implementations supporting TLS I<must> accept this authentication type.
524
525=back
526
527=item the authentication data
528
529The authentication data itself, usually base64 or hex-encoded data, see
530above.
531
532=item the framing protocol chosen
533
534This must be one of the framing protocols offered by the other side in the
535greeting. Each side must accept the choice of the other side.
536
537=back
538
539Example of an authentication reply:
540
541 hmac_md6_64_256;363d5175df38bd9eaddd3f6ca18aa1c0c4aa22f0da245ac638d048398c26b8d3;json
542
543=head2 DATA PHASE
544
545After this, packets get exchanged using the chosen framing protocol. It is
546quite possible that both sides use a different framing protocol.
547
548=head2 FULL EXAMPLE
549
550This is an actual protocol dump of a handshake, followed by a single data
551packet. The greater than/less than lines indicate the direction of the
552transfer only.
553
554 > aemp;0;nndKd+gn;10.0.0.1:4040;hmac_md6_64_256,cleartext;json,storable;provider=AE-0.0;peeraddr=127.0.0.1:1235
555 > sRG8bbc4TDbkpvH8FTP4HBs87OhepH6VuApoZqXXskuG
556 < aemp;0;nmpKd+gh;127.0.0.1:1235,[::1]:1235;hmac_md6_64_256,cleartext;json,storable;provider=AE-0.0;peeraddr=127.0.0.1:58760
557 < dCEUcL/LJVSTJcx8byEsOzrwhzJYOq+L3YcopA5T6EAo
558 > hmac_md6_64_256;9513d4b258975accfcb2ab7532b83690e9c119a502c612203332a591c7237788;json
559 < hmac_md6_64_256;0298d6ba2240faabb2b2e881cf86b97d70a113ca74a87dc006f9f1e9d3010f90;json
560 > ["","lookup","pinger","10.0.0.1:4040#nndKd+gn.a","resolved"]
561
281=head1 SEE ALSO 562=head1 SEE ALSO
282 563
283L<AnyEvent>. 564L<AnyEvent>.
284 565
285=head1 AUTHOR 566=head1 AUTHOR

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines