# $File: //depot/libOurNet/BBS/lib/OurNet/BBS/Client.pm $ $Author: autrijus $ # $Revision: #5 $ $Change: 3958 $ $DateTime: 2003/01/28 02:21:52 $ package OurNet::BBS::Client; use strict; no warnings 'deprecated'; use OurNet::BBS::Base; # Declaration {{{ our ($AUTOLOAD, $Ego, $Port, $NoCache); use overload ( '""' => sub { overload::AddrRef($_[0]) }, '<=>' => sub { "$_[0]" cmp "$_[1]" }, 'cmp' => sub { "$_[0]" cmp "$_[1]" }, 'bool' => sub { 1 }, '0+' => sub { 0 }, '&{}' => sub { my $self = ${$_[0]}; $Ego = $self->[0]; return sub { $AUTOLOAD = 'OurNet::BBS::Client::EXECUTE'; EXECUTE(bless(\[$self, 'CODE_'], __PACKAGE__), @_); }; }, map { my $type = $_; ( SIGILS->[$type].'{}' => sub { my $self = ${$_[0]}; $Ego = $self->[0]; return $self->[$type]; } ); } ( HASH .. ARRAY ), ); use RPC::PlClient; use Digest::MD5 qw/md5/; use OurNet::BBS::Authen; use enum qw/id remote_ref optree/; use enum qw/BITMASK:CIPHER_ NONE BASIC PGP/; use enum qw/BITMASK:AUTH_ NONE CRYPT PGP/; sub UNTIE() {} sub DESTROY() {} # }}} # Initialization {{{ $Port = 7979; my $OP = $OurNet::BBS::Authen::OP; my (%Cache, @delegators, @arguments); tie my %obj => __PACKAGE__, 'HASH_'; tie my @obj => __PACKAGE__, 'ARRAY_'; tie my $code => __PACKAGE__, 'CODE_'; # XXX: not working tie my $glob => __PACKAGE__, 'GLOB_'; # XXX: not working sub TIEHASH { bless(\[$_[1]], $_[0]) } sub TIEARRAY { bless(\[$_[1]], $_[0]) } sub TIESCALAR { bless(\[$_[1]], $_[0]) } use constant IsWin32 => ($^O eq 'MSWin32'); if (IsWin32 and not Win32::IsWinNT()) { require Net::Daemon::Log; no strict 'refs'; no warnings 'redefine'; *{'Net::Daemon::Log'} = sub { return }; *{'Net::Daemon::Log::Log'} = sub { return }; } # }}} sub _spawn { # spawn (optree_id) my $self = [ $Ego->[id], @_ ]; show("SPAWN: @_\n"); # warning: one-arg bless! return bless(\[$self, \%obj, \@obj, \$code, \$glob, 'OBJECT_']); } sub new { my $class = shift; my $peeraddr = shift; my $peerport = shift || $Port; my @args = ( peeraddr => $peeraddr, peerport => $peerport, application => 'OurNet::BBS::Server', version => $OurNet::BBS::Authen::VERSION, ); my $id = @delegators; # 1 more than max $arguments[$id] = [\@args, @_]; return $class->generate($id); } sub generate { my ($class, $id) = @_; my $self = []; $self->[id] = $id; if ($delegators[$id]) { delete $delegators[$id]{client}; $delegators[$id]->DESTROY; } $delegators[$id] = RPC::PlClient->new( @{$arguments[$id][0]} )->ClientObject('__', 'spawn'); my $obj = bless(\[$self, \%obj, \@obj, \$code, \$glob, 'OBJECT_'], $class); return $obj->init(@{$arguments[$id]}[1 .. $#{$arguments[$id]}]); } ## Handshake Phase #################################################### # spawn a handle and get server's accepted modes. {{{ sub init { my ($obj, $keyid, $user, $pass, $cipher_level, $auth_level) = @_; my $self = ${$obj}->[0]; my $client = $delegators[$self->[id]]; unless ($OurNet::BBS::BYPASS_NEGOTIATION) { ($cipher_level, $auth_level) = $client->handshake( OurNet::BBS::Authen->adjust( $cipher_level, $auth_level, $keyid, 1 ) ) or print "[Client] initialization failed.\n" and die; my ($status, $auth) = negotiate_cipher($client, $cipher_level) or print "[Client] cipher negotiation failed.\n" and die; negotiate_auth($client, $auth_level, $auth, $keyid, $user, $pass) or print "[Client] authentication failed.\n" and die; $self->[remote_ref] = negotiate_locate($client) or print "[Client] object location failed.\n" and die; } show("done!\n"); return $obj; } sub negotiate_locate { my $client = shift; return $client->locate(@_); } sub make_auth { my ($keyid, $pubkey) = @_; my $auth = OurNet::BBS::Authen->new($keyid) or return; $auth->import_key($pubkey); return $auth; } # }}} ## Cipher Phase ####################################################### # gets supported cipher suites and (optionally) server's public key {{{ sub negotiate_cipher { my ($client, $mode, $auth) = @_; my $cipher = OurNet::BBS::Authen->suites($client->get_suites) if $mode & (CIPHER_BASIC | CIPHER_PGP); show("[Client] agreed on cipher: $cipher ") if $cipher; if ($cipher and $mode & CIPHER_PGP) { $auth = make_auth($client->get_pubkey); if ($auth and cipher_pgp($client, $cipher, $auth)) { show("in secure mode.\n"); return(CIPHER_PGP, $auth); } } if ($cipher and $mode & CIPHER_BASIC) { if (cipher_basic($client, $cipher)) { show("in insecure mode.\n"); return(CIPHER_BASIC, $auth); } } if ($mode & CIPHER_NONE and cipher_none($client)) { show("[Client] warning: using plaintext communication.\n"); return(CIPHER_NONE, $auth); } show("failed!\n"); return; } sub cipher_pgp { my ($client, $cipher, $auth) = @_; my $keysize = $cipher->keysize || ( $cipher eq 'Crypt::Blowfish' ? 56 : 8 ); # make session key my $session_key = md5(rand); $session_key .= md5(rand) until length($session_key) >= $keysize; $session_key = substr($session_key, 0, $keysize); my $authcrypt = $auth->encrypt($session_key) or return; # encrypt it $client->cipher_pgp($cipher, $authcrypt) or return; # send it back $client->{client}{cipher} = $cipher->new($session_key); return $auth; } sub cipher_basic { my ($client, $cipher) = @_; my ($status, $session) = $client->cipher_basic($cipher) or return; return ($client->{client}{cipher} = $cipher->new($session)); } sub cipher_none { my ($client) = @_; return $client->cipher_none; } # }}} ## Auth Phase ######################################################### # log in by trying each mutually acceptable authentication schemes {{{ sub negotiate_auth { my ($client, $mode, $auth, $keyid, $user, $pass) = @_; # Authentication Negotiation show("[Client] begin authentication..."); if ($mode & AUTH_PGP and $auth ||= make_auth($client->get_pubkey)) { # public key authentication show("trying pubkey..."); return AUTH_PGP if auth_pgp( $client, $auth, $keyid, $user, $pass ); } if ($mode & AUTH_CRYPT and $user) { # crypt-based authentication show("trying crypt..."); return AUTH_CRYPT if auth_crypt($client, $user, $pass); } if ($mode & AUTH_NONE and $client->auth_none($user)) { # no authentication at all show("fallback to none..."); return AUTH_NONE; } show("failed!\n"); return; } sub auth_pgp { my ($client, $auth, $keyid, $login, $passphrase) = @_; return unless $keyid and $login and defined $passphrase; $auth->{keyid} = $keyid; $auth->setpass($passphrase); my $challenge = $client->auth_pgp($login); if ($challenge eq $OP->{STATUS_NO_USER}) { show('no such user! '); return; } elsif ($challenge eq $OP->{STATUS_NO_PUBKEY}) { show('no public key info! '); return; } elsif ($challenge eq $OP->{STATUS_OK}) { show("challenge($challenge)"); $challenge = $client->set_pubkey($auth->export_key); } if ($challenge eq $OP->{STATUS_BAD_PUBKEY}) { show('public key mismatch! '); return; } my $signature = $auth->clearsign($challenge) or (show('cannot make signature! ') and return); if ($client->set_sign($signature) eq $OP->{STATUS_BAD_SIGNATURE}) { show('signature rejected! '); return; } return 1; } sub auth_crypt { my ($client, $user, $pass) = @_; my ($status, $salt) = $client->auth_crypt($user) or return; if ($status eq $OP->{STATUS_NO_USER}) { show('no such user! '); return; } return ( $client->set_crypted(crypt($pass, $salt)) eq $OP->{STATUS_ACCEPTED} ); } sub auth_none { my ($client) = @_; return $client->auth_none; } sub quit { foreach my $client (@delegators) { $client->quit if $client; } undef @delegators; } sub show { no warnings 'once'; print $_[0] if $OurNet::BBS::DEBUG; } sub register_callback { my $coderef = shift; my $proxy = bless(\"$coderef", '__CODE__'); show("$coderef registered for callback\n"); $RPC::PlServer::Comm::Callback{"$coderef"} = $coderef; return $proxy; } # }}} ## Connected ########################################################## # do the real job via AUTOLOAD passing and ArrayHashMonster magic {{{ sub AUTOLOAD { my ($ego, $op); no strict 'refs'; return unless $delegators[$Ego->[id]]; my $action = substr($AUTOLOAD, ( (rindex($AUTOLOAD, ':') + 1) || return )); # install a closure-based handler for future use instead of AUTOLOAD *{$AUTOLOAD} = sub { no warnings 'uninitialized'; my ($self, $op) = @{${+shift}}[0, -1]; local $Ego = $self if ($op eq 'OBJECT_'); $op .= $action; my @result; do { eval { undef $@; @result = $delegators[$Ego->[id]]->__( $OP->{$op} || $op, $Ego->[optree], map { ref($_) eq __PACKAGE__ ? bless(\(${$_}->[0][optree]), '__') : ref($_) eq 'CODE' ? register_callback($_) : $_; } @_ ); } } while ( $@ and $@ =~ /^Error while reading socket:/ and __PACKAGE__->generate($Ego->[id]) ); die $@ if $@; if (@result == 4 and !$result[0] and my $opcode = $result[1]) { return ($NoCache ? _spawn(@result[2, 3]) : ($Cache{$result[2]} ||= _spawn(@result[2, 3]))) if $OP->{$opcode} eq 'OBJECT_SPAWN'; return @result if $OP->{$opcode} eq 'STATUS_IGNORED'; die "@result[2, 3] [$OP->{$opcode}]\n"; } # print ("<==: ".(wantarray ? "@result" : $result[0]), "\n"); return wantarray ? @result : $result[0]; } unless exists(&{$AUTOLOAD}); goto &{$AUTOLOAD}; } # }}} 1;