=head1 NAME AnyEvent::MP::Transport - actual transport protocol =head1 SYNOPSIS use AnyEvent::MP::Transport; =head1 DESCRIPTION This is the superclass for MP transports, most of which is considered an implementation detail. See the "PROTOCOL" section below if you want to write another client for this protocol. =head1 FUNCTIONS/METHODS =over 4 =cut package AnyEvent::MP::Transport; use common::sense; use Scalar::Util; use MIME::Base64 (); use Storable (); use JSON::XS (); use AE (); use AnyEvent::Socket (); use AnyEvent::Handle (); use base Exporter::; our $VERSION = '0.0'; our $PROTOCOL_VERSION = 0; =item $listener = mp_listener $host, $port, , $cb->($transport) Creates a listener on the given host/port using C. See C, below, for constructor arguments. Defaults for peerhost, peerport, fh and tls are provided. =cut sub mp_server($$@) { my $cb = pop; my ($host, $port, @args) = @_; AnyEvent::Socket::tcp_server $host, $port, sub { my ($fh, $host, $port) = @_; $cb->(new AnyEvent::MP::Transport fh => $fh, peerhost => $host, peerport => $port, tls => "accept", @args, ); } } =item $guard = mp_connect $host, $port, , $cb->($transport) =cut sub mp_connect { my $cb = pop; my ($host, $port, @args) = @_; AnyEvent::Socket::tcp_connect $host, $port, sub { my ($fh, $nhost, $nport) = @_; return $cb->() unless $fh; $cb->(new AnyEvent::MP::Transport fh => $fh, peername => $host, peerhost => $nhost, peerport => $nport, tls => "accept", @args, ); } } =item new AnyEvent::MP::Transport # immediately starts negotiation my $transport = new AnyEvent::MP::Transport # mandatory fh => $filehandle, local_id => $identifier, on_recv => sub { receive-callback }, on_error => sub { error-callback }, # optional secret => "shared secret", on_eof => sub { clean-close-callback }, on_connect => sub { successful-connect-callback }, greeting => { key => value }, # tls support tls => "accept|connect", tls_ctx => AnyEvent::TLS, peername => $peername, # for verification ; =cut our @FRAMINGS = qw(json storable); # the framing types we accept and send, in order of preference our @AUTH_SND = qw(hmac_md6_64_256); # auth types we send our @AUTH_RCV = (@AUTH_SND, qw(hex_secret)); # auth types we accept #AnyEvent::Handle::register_write_type mp_record => sub { #}; sub new { my ($class, %arg) = @_; my $self = bless \%arg, $class; $self->{queue} = []; { Scalar::Util::weaken (my $self = $self); if (exists $arg{connect}) { $arg{tls} ||= "connect"; $arg{tls_ctx} ||= { sslv2 => 0, sslv3 => 0, tlsv1 => 1, verify => 1 }; } $arg{secret} = AnyEvent::MP::Base::default_secret () unless exists $arg{secret}; $self->{hdl} = new AnyEvent::Handle fh => delete $arg{fh}, rbuf_max => 64 * 1024, autocork => 1, no_delay => 1, on_error => sub { $self->error ($_[2]); }, peername => delete $arg{peername}, ; my $secret = $arg{secret}; my $greeting_kv = $self->{greeting} ||= {}; $greeting_kv->{"tls1.0"} ||= $arg{tls} if exists $arg{tls} && $arg{tls_ctx}; $greeting_kv->{provider} = "AE-$VERSION"; $greeting_kv->{peeraddr} = AnyEvent::Socket::format_hostport $self->{peerhost}, $self->{peerport}; # send greeting my $lgreeting1 = "aemp;$PROTOCOL_VERSION;$PROTOCOL_VERSION" # version, min . ";$AnyEvent::MP::Base::UNIQ" . ";$AnyEvent::MP::Base::NODE" . ";" . (join ",", @AUTH_RCV) . ";" . (join ",", @FRAMINGS) . (join "", map ";$_=$greeting_kv->{$_}", keys %$greeting_kv); my $lgreeting2 = MIME::Base64::encode_base64 AnyEvent::MP::Base::nonce (33), ""; $self->{hdl}->push_write ("$lgreeting1\012$lgreeting2\012"); # expect greeting $self->{hdl}->push_read (line => sub { my $rgreeting1 = $_[1]; my ($aemp, $version, $version_min, $uniq, $rnode, $auths, $framings, @kv) = split /;/, $rgreeting1; if ($aemp ne "aemp") { return $self->error ("unparsable greeting"); } elsif ($version_min > $PROTOCOL_VERSION) { return $self->error ("version mismatch (we: $PROTOCOL_VERSION, they: $version_min .. $version)"); } my $s_auth; for my $auth_ (split /,/, $auths) { if (grep $auth_ eq $_, @AUTH_SND) { $s_auth = $auth_; last; } } defined $s_auth or return $self->error ("$auths: no common auth type supported"); die unless $s_auth eq "hmac_md6_64_256"; # hardcoded atm. my $s_framing; for my $framing_ (split /,/, $framings) { if (grep $framing_ eq $_, @FRAMINGS) { $s_framing = $framing_; last; } } defined $s_framing or return $self->error ("$framings: no common framing method supported"); $self->{remote_uniq} = $uniq; $self->{remote_node} = $rnode; $self->{remote_greeting} = { map /^([^=]+)(?:=(.*))?/ ? ($1 => $2) : (), @kv }; if (exists $self->{tls} and $self->{tls_ctx} and exists $self->{remote_greeting}{"tls1.0"}) { if ($self->{tls} ne $self->{remote_greeting}{"tls1.0"}) { return $self->error ("TLS server/client mismatch"); } $self->{hdl}->starttls ($self->{tls}, $self->{tls_ctx}); } # read nonce $self->{hdl}->push_read (line => sub { my $rgreeting2 = $_[1]; # auth require Digest::MD6; require Digest::HMAC_MD6; my $key = Digest::MD6::md6_hex ($secret); my $lauth = Digest::HMAC_MD6::hmac_md6_base64 ($key, "$lgreeting1\012$lgreeting2\012$rgreeting1\012$rgreeting2\012", 64, 256); my $rauth = $s_auth eq "hmac_md6_64_256" ? Digest::HMAC_MD6::hmac_md6_base64 ($key, "$rgreeting1\012$rgreeting2\012$lgreeting1\012$lgreeting2\012", 64, 256) : $s_auth eq "hex_secret" ? unpack "H*", $secret : die; $lauth ne $rauth # echo attack? or return $self->error ("authentication error"); $self->{hdl}->push_write ("$s_auth;$lauth;$s_framing\012"); $self->{hdl}->rbuf_max (64); # enough for 44 reply bytes or so $self->{hdl}->push_read (line => sub { my ($hdl, $rline) = @_; my ($auth_method, $rauth2, $r_framing) = split /;/, $rline; if ($rauth2 ne $rauth) { return $self->error ("authentication failure/shared secret mismatch"); } $self->{s_framing} = $s_framing; $hdl->rbuf_max (undef); my $queue = delete $self->{queue}; # we are connected $self->connected; $hdl->push_write ($self->{s_framing} => $_) for @$queue; my $rmsg; $rmsg = sub { $_[0]->push_read ($r_framing => $rmsg); AnyEvent::MP::Base::_inject ($_[1]); }; $hdl->push_read ($r_framing => $rmsg); }); }); }); } $self } sub error { my ($self, $msg) = @_; if ($self->{node} && $self->{node}{transport} == $self) { $self->{node}->clr_transport; } $AnyEvent::MP::Base::WARN->("$self->{peerhost}:$self->{peerport}: $msg"); $self->destroy; } sub connected { my ($self) = @_; my $node = AnyEvent::MP::Base::add_node ($self->{remote_node}); Scalar::Util::weaken ($self->{node} = $node); $node->set_transport ($self); } sub send { $_[0]{hdl}->push_write ($_[0]{s_framing} => $_[1]); } sub destroy { my ($self) = @_; $self->{hdl}->destroy if $self->{hdl}; } sub DESTROY { my ($self) = @_; $self->destroy; } =back =head1 PROTOCOL The protocol is relatively simple, and consists of three phases which are symmetrical for both sides: greeting (followed by optionally switching to TLS mode), authentication and packet exchange. the protocol is designed to allow both full-text and binary streams. The greeting consists of two text lines that are ended by either an ASCII CR LF pair, or a single ASCII LF (recommended). =head2 GREETING The first line contains strings seperated (not ended) by C<;> characters. The first seven strings are fixed by the protocol, the remaining strings are C pairs. None of them may contain C<;> characters themselves. The seven fixed strings are: =over 4 =item C The constant C to identify the protocol. =item protocol version The (maximum) protocol version supported by this end, currently C<0>. =item minimum protocol version The minimum protocol version supported by this end, currently C<0>. =item a token uniquely identifying the current node instance This is a string that must change between restarts. It usually contains things like the current time, the (OS) process id or similar values, but no meaning of the contents are assumed. =item the node endpoint descriptors for public nodes, this is a comma-separated list of protocol endpoints, i.e., the noderef. For slave nodes, this is a unique identifier. =item the acceptable authentication methods A comma-separated list of authentication methods supported by the node. Note that AnyEvent::MP supports a C authentication method that accepts a cleartext password (hex-encoded), but will not use this auth method itself. The receiving side should choose the first auth method it supports. =item the acceptable framing formats A comma-separated list of packet encoding/framign formats understood. The receiving side should choose the first framing format it supports for sending packets (which might be different from the format it has to accept). . ";$self->{peerhost};$self->{peerport}" . (join "", map ";$_=$greeting_kv->{$_}", keys %$greeting_kv); my $lgreeting2 = MIME::Base64::encode_base64 AnyEvent::MP::Base::nonce (33), ""; =head1 SEE ALSO L. =head1 AUTHOR Marc Lehmann http://home.schmorp.de/ =cut 1