=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_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->{"tls"} = "1.0" if $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 }; # read nonce $self->{hdl}->push_read (line => sub { my $rgreeting2 = $_[1]; if ($self->{tls_ctx} and 1 == int $self->{remote_greeting}{"tls"}) { $self->{tls} = $lgreeting2 lt $rgreeting2 ? "connect" : "accept"; $self->{hdl}->starttls ($self->{tls}, $self->{tls_ctx}); } # 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 separated (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). =cut The remaining arguments are C pairs. The following key-value pairs are known at this time: =over 4 =item provider= The software provider for this implementation. For AnyEvent::MP, this is C or whatever version it currently is at. =item peeraddr=: The peer address (socket address of the other side) as seen locally, in the same format as noderef endpoints. =item tls=. Indicates that the other side supports TLS (version should be 1.0) and wishes to do a TLS handshake. =back After this greeting line there will be a second line containing a cryptographic nonce, i.e. random data of high quality. To keep the protocol text-only, these are usually 32 base64-encoded octets, but it could be anything that doesn't contain any ASCII CR or ASCII LF characters. Example of the two lines of greeting: aemp;0;0;e7d.4a76f48f;10.0.0.1:4040;hmac_md6_64_256,hex_secret;json,storable;provider=AE-0.0;peeraddr=127.0.0.1:1235 XntegV2Guvss0qNn7phCPnoU87xqxV+4Mqm/5y4iQm6a =head2 TLS handshake If, after the handshake, both sides indicate interest in TLS, then the connection I use TLS, or fail. Both sides compare their nonces, and the side who sent the lower nonce value ("string" comparison on the raw octet values) becomes the client, and the one with the higher nonce the server. =head2 AUTHENTICATION PHASE After the greeting is received (and the optional TLS handshake), the authentication phase begins, which consists of sending a single C<;>-separated line with three fixed strings and any number of C pairs. The three fixed strings are: =over 4 =item the authentication method chosen This must be one of the methods offered by the other side in the greeting. =item the authentication data The authentication data itself, usually base64 or hex-encoded data. =item the framing protocol chosen This must be one of the framing protocols offered by the other side in the greeting. Each side must accept the choice of the other side. =back =head2 DATA PHASE After this, packets get exchanged using the chosen framing protocol. It is quite possible that both sides use a different framing protocol. =head1 SEE ALSO L. =head1 AUTHOR Marc Lehmann http://home.schmorp.de/ =cut 1