Annotation of loncom/LondConnection.pm, revision 1.1
1.1 ! foxr 1: #
! 2: # This module defines and implements a class that represents
! 3: # a connection to a lond daemon.
! 4: package LondConnection;
! 5:
! 6: use IO::Socket;
! 7: use IO::Socket::INET;
! 8: use IO::Handle;
! 9: use IO::File;
! 10: use Fcntl;
! 11: use POSIX;
! 12: use Crypt::IDEA;
! 13: use LONCAPA::Configuration;
! 14: use LONCAPA::HashIterator;
! 15:
! 16: my $DebugLevel=4;
! 17:
! 18: # Read the configuration file for apache to get the perl
! 19: # variable set.
! 20:
! 21: my $perlvarref = LONCAPA::Configuration::read_conf('loncapa.conf');
! 22: my %perlvar = %{$perlvarref};
! 23: my $hoststab =
! 24: LONCAPA::Configuration::read_hosts(
! 25: "$perlvar{'lonTabDir'}/hosts.tab") ||
! 26: die "Can't read host table!!";
! 27: my %hostshash = %{$hoststab};
! 28:
! 29: close(CONFIG);
! 30:
! 31: sub Debug {
! 32: my $level = shift;
! 33: my $message = shift;
! 34: if ($level < $DebugLevel) {
! 35: print($message."\n");
! 36: }
! 37: }
! 38: =pod
! 39: Dump the internal state of the object: For debugging purposes.
! 40: =cut
! 41:
! 42: sub Dump {
! 43: my $self = shift;
! 44: print "Dumping LondConnectionObject:\n";
! 45: while(($key, $value) = each %$self) {
! 46: print "$key -> $value\n";
! 47: }
! 48: print "-------------------------------\n";
! 49: }
! 50:
! 51: =pod
! 52: Local function to do a state transition. If the state transition callback
! 53: is defined it is called with two parameters: the self and the old state.
! 54: =cut
! 55: sub Transition {
! 56: my $self = shift;
! 57: my $newstate = shift;
! 58: my $oldstate = $self->{State};
! 59: $self->{State} = $newstate;
! 60: $self->{TimeoutRemaining} = $self->{TimeoutValue};
! 61: if($self->{TransitionCallback}) {
! 62: ($self->{TransitionCallback})->($self, $oldstate);
! 63: }
! 64: }
! 65:
! 66: =pod
! 67: Construct a new lond connection.
! 68: Parameters (besides the class name) include:
! 69: =item hostname - host the remote lond is on.
! 70: This host is a host in the hosts.tab file
! 71: =item port - port number the remote lond is listening on.
! 72: =cut
! 73: sub new {
! 74: my $class = shift; # class name.
! 75: my $Hostname = shift; # Name of host to connect to.
! 76: my $Port = shift; # Port to connect
! 77: &Debug(4,$class."::new( ".$Hostname.",".$Port.")\n");
! 78:
! 79: # The host must map to an entry in the hosts table:
! 80: # We connect to the dns host that corresponds to that
! 81: # system and use the hostname for the encryption key
! 82: # negotion. In the objec these become the Host and
! 83: # LoncapaHim fields of the object respectively.
! 84: #
! 85: if (!exists $hostshash{$Hostname}) {
! 86: return undef; # No such host!!!
! 87: }
! 88: my @ConfigLine = @{$hostshash{$Hostname}};
! 89: my $DnsName = $ConfigLine[3]; # 4'th item is dns of host.
! 90: Debug(5, "Connecting to ".$DnsName);
! 91: # Now create the object...
! 92: my $self = { Host => $DnsName,
! 93: LoncapaHim => $Hostname,
! 94: Port => $Port,
! 95: State => "Initialized",
! 96: TransactionRequest => "",
! 97: TransactionReply => "",
! 98: InformReadable => 0,
! 99: InformWritable => 0,
! 100: TimeoutCallback => undef,
! 101: TransitionCallback => undef,
! 102: Timeoutable => 0,
! 103: TimeoutValue => 60,
! 104: TimeoutRemaining => 0,
! 105: CipherKey => "",
! 106: Cipher => undef};
! 107: bless($self, $class);
! 108: unless ($self->{Socket} = IO::Socket::INET->new(PeerHost => $self->{Host},
! 109: PeerPort => $self->{Port},
! 110: Type => SOCK_STREAM,
! 111: Proto => "tcp")) {
! 112: return undef; # Inidicates the socket could not be made.
! 113: }
! 114: #
! 115: # We're connected. Set the state, and the events we'll accept:
! 116: #
! 117: $self->Transition("Connected");
! 118: $self->{InformWritable} = 1; # When socket is writable we send init
! 119: $self->{TransactionRequest} = "init\n";
! 120:
! 121: #
! 122: # Set socket to nonblocking I/O.
! 123: #
! 124: my $socket = $self->{Socket};
! 125: $flags = fcntl($socket->fileno, F_GETFL,0);
! 126: if($flags == -1) {
! 127: $socket->close;
! 128: return undef;
! 129: }
! 130: if(fcntl($socket, F_SETFL, $flags | O_NONBLOCK) == -1) {
! 131: $socket->close;
! 132: return undef;
! 133: }
! 134:
! 135: # return the object :
! 136:
! 137: return $self;
! 138: }
! 139: =pod
! 140: This member should be called when the Socket becomes readable.
! 141: Until the read completes, action is state independet. Data are accepted
! 142: into the TransactionReply until a newline character is received. At that
! 143: time actionis state dependent:
! 144: =item Connected: in this case we received challenge, the state changes
! 145: to ChallengeReceived, and we initiate a send with the challenge response.
! 146: =item ReceivingReply: In this case a reply has been received for a transaction,
! 147: the state goes to Idle and we disable write and read notification.
! 148: =item ChallengeReeived: we just got what should be an ok\n and the
! 149: connection can now handle transactions.
! 150:
! 151: =cut
! 152: sub Readable {
! 153: my $self = shift;
! 154: my $socket = $self->{Socket};
! 155: my $data = '';
! 156: my $rv = $socket->recv($data, POSIX::BUFSIZ, 0);
! 157: my $errno = $! + 0; # Force numeric context.
! 158:
! 159: unless (defined($rv) && length($data)) { # Read failed,
! 160: if(($errno == POSIX::EWOULDBLOCK) ||
! 161: ($errno == POSIX::EAGAIN) ||
! 162: ($errno == POSIX::EINTR) ||
! 163: ($errno == 0)) {
! 164: return 0;
! 165: }
! 166:
! 167: # Connection likely lost.
! 168: &Debug(4, "Connection lost");
! 169: $self->{TransactionRequest} = '';
! 170: $socket->close();
! 171: $self->Transition("Disconnected");
! 172: return -1;
! 173: }
! 174: # Append the data to the buffer. And figure out if the read is done:
! 175:
! 176: &Debug(9,"Received from host: ".$data);
! 177: $self->{TransactionReply} .= $data;
! 178: if($self->{TransactionReply} =~ /(.*\n)/) {
! 179: &Debug(8,"Readable End of line detected");
! 180: if ($self->{State} eq "Initialized") { # We received the challenge:
! 181: if($self->{TransactionReply} eq "refused") { # Remote doesn't have
! 182:
! 183: $self->Transition("Disconnected"); # in host tables.
! 184: $socket->close();
! 185: return -1;
! 186: }
! 187:
! 188: &Debug(8," Transition out of Initialized");
! 189: $self->{TransactionRequest} = $self->{TransactionReply};
! 190: $self->{InformWritable} = 1;
! 191: $self->{InformReadable} = 0;
! 192: $self->Transition("ChallengeReceived");
! 193: $self->{TimeoutRemaining} = $self->{TimeoutValue};
! 194: return 0;
! 195: } elsif ($self->{State} eq "ChallengeReplied") { # should be ok.
! 196: if($self->{TransactionReply} != "ok\n") {
! 197: $self->Transition("Disconnected");
! 198: $socket->close();
! 199: return -1;
! 200: }
! 201: $self->Transition("RequestingKey");
! 202: $self->{InformReadable} = 0;
! 203: $self->{InformWritable} = 1;
! 204: $self->{TransactionRequest} = "ekey\n";
! 205: return 0;
! 206: } elsif ($self->{State} eq "ReceivingKey") {
! 207: my $buildkey = $self->{TransactionReply};
! 208: my $key = $self->{LoncapaHim}.$perlvar{'lonHostID'};
! 209: $key=~tr/a-z/A-Z/;
! 210: $key=~tr/G-P/0-9/;
! 211: $key=~tr/Q-Z/0-9/;
! 212: $key=$key.$buildkey.$key.$buildkey.$key.$buildkey;
! 213: $key=substr($key,0,32);
! 214: my $cipherkey=pack("H32",$key);
! 215: $self->{Cipher} = new IDEA $cipherkey;
! 216: if($self->{Cipher} == undef) {
! 217: $self->Transition("Disconnected");
! 218: $socket->close();
! 219: return -1;
! 220: } else {
! 221: $self->Transition("Idle");
! 222: $self->{InformWritable} = 0;
! 223: $self->{InformReadable} = 0;
! 224: $self->{Timeoutable} = 0;
! 225: return 0;
! 226: }
! 227: } elsif ($self->{State} eq "ReceivingReply") {
! 228:
! 229: # If the data are encrypted, decrypt first.
! 230:
! 231: my $answer = $self->{TransactionReply};
! 232: if($answer =~ /^enc\:/) {
! 233: $answer = $self->Decrypt($answer);
! 234: $self->{TransactionReply} = $answer;
! 235: }
! 236:
! 237: # finish the transaction
! 238:
! 239: $self->{InformWritable} = 0;
! 240: $self->{InformReadable} = 0;
! 241: $self->{Timeoutable} = 0;
! 242: $self->Transition("Idle");
! 243: return 0;
! 244: } elsif ($self->{State} eq "Disconnected") { # No connection.
! 245: return -1;
! 246: } else { # Internal error: Invalid state.
! 247: $self->Transition("Disconnected");
! 248: $socket->close();
! 249: return -1;
! 250: }
! 251: }
! 252:
! 253: return 0;
! 254:
! 255: }
! 256:
! 257:
! 258: =pod
! 259: This member should be called when the Socket becomes writable.
! 260: The action is state independent. An attempt is made to drain the contents of
! 261: the TransactionRequest member. Once this is drained, we mark the object
! 262: as waiting for readability.
! 263:
! 264: Returns 0 if successful, or -1 if not.
! 265:
! 266: =cut
! 267: sub Writable {
! 268: my $self = shift; # Get reference to the object.
! 269: my $socket = $self->{Socket};
! 270: my $nwritten = $socket->send($self->{TransactionRequest}, 0);
! 271: my $errno = $! + 0;
! 272: unless (defined $nwritten) {
! 273: if($errno != POSIX::EINTR) {
! 274: $self->Transition("Disconnected");
! 275: return -1;
! 276: }
! 277:
! 278: }
! 279: if (($rv >= 0) ||
! 280: ($errno == POSIX::EWOULDBLOCK) ||
! 281: ($errno == POSIX::EAGAIN) ||
! 282: ($errno == POSIX::EINTR) ||
! 283: ($errno == 0)) {
! 284: substr($self->{TransactionRequest}, 0, $nwritten) = ""; # rmv written part
! 285: if(length $self->{TransactionRequest} == 0) {
! 286: $self->{InformWritable} = 0;
! 287: $self->{InformReadable} = 1;
! 288: $self->{TransactionReply} = '';
! 289: #
! 290: # Figure out the next state:
! 291: #
! 292: if($self->{State} eq "Connected") {
! 293: $self->Transition("Initialized");
! 294: } elsif($self->{State} eq "ChallengeReceived") {
! 295: $self->Transition("ChallengeReplied");
! 296: } elsif($self->{State} eq "RequestingKey") {
! 297: $self->Transition("ReceivingKey");
! 298: $self->{InformWritable} = 0;
! 299: $self->{InformReadable} = 1;
! 300: $self->{TransactionReply} = '';
! 301: } elsif ($self->{State} eq "SendingRequest") {
! 302: $self->Transition("ReceivingReply");
! 303: $self->{TimeoutRemaining} = $self->{TimeoutValue};
! 304: } elsif ($self->{State} eq "Disconnected") {
! 305: return -1;
! 306: }
! 307: return 0;
! 308: }
! 309: } else { # The write failed (e.g. partner disconnected).
! 310: $self->Transition("Disconnected");
! 311: $socket->close();
! 312: return -1;
! 313: }
! 314:
! 315: }
! 316: =pod
! 317: Tick is called every time unit by the event framework. It
! 318: 1. decrements the remaining timeout.
! 319: 2. If the timeout is zero, calls TimedOut indicating that the
! 320: current operation timed out.
! 321:
! 322: =cut
! 323:
! 324: sub Tick {
! 325: my $self = shift;
! 326: $self->{TimeoutRemaining}--;
! 327: if ($self->{TimeoutRemaining} < 0) {
! 328: $self->TimedOut();
! 329: }
! 330: }
! 331: =pod
! 332: TimedOut - called on a timeout. If the timeout callback is defined,
! 333: it is called with $self as its parameters.
! 334:
! 335: =cut
! 336: sub TimedOut {
! 337:
! 338: my $self = shift;
! 339: if($self->{TimeoutCallback}) {
! 340: my $callback = $self->{TimeoutCallback};
! 341: my @args = ( $self);
! 342: &$callback(@args);
! 343: }
! 344: }
! 345: =pod
! 346: Called to initiate a transaction. A transaction can only be initiated
! 347: when the object is idle... otherwise an error is returned.
! 348: A transaction consists of a request to the server that will have a reply.
! 349: This member sets the request data in the TransactionRequest member,
! 350: makes the state SendingRequest and sets the data to allow a timout,
! 351: and to request writability notification.
! 352: =cut
! 353: sub InitiateTransaction {
! 354: my $self = shift;
! 355: my $data = shift;
! 356:
! 357: if($self->{State} ne "Idle") {
! 358: return -1; # Error indicator.
! 359: }
! 360: # if the transaction is to be encrypted encrypt the data:
! 361:
! 362: if($data =~ /^encrypt\:/) {
! 363: $data = $self->Encrypt($data);
! 364: }
! 365:
! 366: # Setup the trasaction
! 367:
! 368: $self->{TransactionRequest} = $data;
! 369: $self->{TransactionReply} = "";
! 370: $self->{InformWritable} = 1;
! 371: $self->{InformReadable} = 0;
! 372: $self->{Timeoutable} = 1;
! 373: $self->{TimeoutRemaining} = $self->{TimeoutValue};
! 374: $self->Transition("SendingRequest");
! 375: }
! 376:
! 377:
! 378: =pod
! 379: Sets a callback for state transitions. Returns a reference to any
! 380: prior established callback, or undef if there was none:
! 381: =cut
! 382: sub SetStateTransitionCallback {
! 383: my $self = shift;
! 384: my $oldCallback = $self->{TransitionCallback};
! 385: $self->{TransitionCallback} = shift;
! 386: return $oldCallback;
! 387: }
! 388: =pod
! 389: Sets the timeout callback. Returns a reference to any prior established
! 390: callback or undef if there was none.
! 391: =cut
! 392: sub SetTimeoutCallback {
! 393: my $self = shift;
! 394: my $callback = shift;
! 395: my $oldCallback = $self->{TimeoutCallback};
! 396: $self->{TimeoutCallback} = $callback;
! 397: return $oldCallback;
! 398: }
! 399:
! 400: =pod
! 401: GetState - selector for the object state.
! 402: =cut
! 403: sub GetState {
! 404: my $self = shift;
! 405: return $self->{State};
! 406: }
! 407: =pod
! 408: GetSocket - selector for the object socket.
! 409: =cut
! 410: sub GetSocket {
! 411: my $self = shift;
! 412: return $self->{Socket};
! 413: }
! 414: =pod
! 415: Return the state of the flag that indicates the object wants to be
! 416: called when readable.
! 417: =cut
! 418: sub WantReadable {
! 419: my $self = shift;
! 420:
! 421: return $self->{InformReadable};
! 422: }
! 423: =pod
! 424: Return the state of the flag that indicates the object wants write
! 425: notification.
! 426: =cut
! 427: sub WantWritable {
! 428: my $self = shift;
! 429: return $self->{InformWritable};
! 430: }
! 431: =pod
! 432: return the state of the flag that indicates the object wants to be informed
! 433: of timeouts.
! 434: =cut
! 435: sub WantTimeout {
! 436: my $self = shift;
! 437: return $self->{Timeoutable};
! 438: }
! 439:
! 440: =pod
! 441: Returns the reply from the last transaction.
! 442: =cut
! 443: sub GetReply {
! 444: my $self = shift;
! 445: return $self->{TransactionReply};
! 446: }
! 447:
! 448: =pod
! 449: Returns the encrypted version of the command string.
! 450: The command input string is of the form:
! 451: encrypt:command
! 452: The output string can be directly sent to lond as it's of the form:
! 453: enc:length:<encodedrequest>
! 454: '
! 455: =cut
! 456: sub Encrypt {
! 457: my $self = shift; # Reference to the object.
! 458: my $request = shift; # Text to send.
! 459:
! 460:
! 461: # Split the encrypt: off the request and figure out it's length.
! 462: # the cipher works in blocks of 8 bytes.
! 463:
! 464: my $cmd = $request;
! 465: $cmd =~ s/^encrypt\://; # strip off encrypt:
! 466: chomp($cmd); # strip off trailing \n
! 467: my $length=length($cmd); # Get the string length.
! 468: $cmd .= " "; # Pad with blanks so we can fill out a block.
! 469:
! 470: # encrypt the request in 8 byte chunks to create the encrypted
! 471: # output request.
! 472:
! 473: my $Encoded = '';
! 474: for(my $index = 0; $index <= $length; $index += 8) {
! 475: $Encoded .=
! 476: unpack("H16",
! 477: $self->{Cipher}->encrypt(substr($cmd,
! 478: $index, 8)));
! 479: }
! 480:
! 481: # Build up the answer as enc:length:$encrequest.
! 482:
! 483: $request = "enc:$length:$Encoded\n";
! 484: return $request;
! 485:
! 486:
! 487: }
! 488: =pod
! 489: Decrypt
! 490: Decrypt a response from the server. The response is in the form:
! 491: enc:<length>:<encrypted data>
! 492: =cut
! 493: sub Decrypt {
! 494: my $self = shift; # Recover reference to object
! 495: my $encrypted = shift; # This is the encrypted data.
! 496:
! 497: # Bust up the response into length, and encryptedstring:
! 498:
! 499: my ($enc, $length, $EncryptedString) = split(/:/,$encrypted);
! 500: chomp($EncryptedString);
! 501:
! 502: # Decode the data in 8 byte blocks. The string is encoded
! 503: # as hex digits so there are two characters per byte:
! 504:
! 505: $decrpyted = "";
! 506: for(my $index = 0; $index < length($EncryptedString);
! 507: $index += 16) {
! 508: $decrypted .= $self->{Cipher}->decrypt(
! 509: pack("H16",
! 510: substr($EncryptedString,
! 511: $index,
! 512: 16)));
! 513: }
! 514: # the answer may have trailing pads to fill out a block.
! 515: # $length tells us the actual length of the decrypted string:
! 516:
! 517: $decrypted = substr($decrypted, 0, $length);
! 518:
! 519: return $decrypted;
! 520:
! 521: }
! 522:
! 523: =pod
! 524: =head GetHostIterator
! 525:
! 526: Returns a hash iterator to the host information. Each get from
! 527: this iterator returns a reference to an array that contains
! 528: information read from the hosts configuration file. Array elements
! 529: are used as follows:
! 530:
! 531: [0] - LonCapa host name.
! 532: [1] - LonCapa domain name.
! 533: [2] - Loncapa role (e.g. library or access).
! 534: [3] - DNS name server hostname.
! 535: [4] - IP address (result of e.g. nslooup [3]).
! 536: [5] - Maximum connection count.
! 537: [6] - Idle timeout for reducing connection count.
! 538: [7] - Minimum connection count.
! 539:
! 540:
! 541: =cut
! 542: sub GetHostIterator {
! 543:
! 544: return HashIterator->new(\%hostshash);
! 545: }
! 546:
! 547: 1;
! 548:
! 549: =pod
! 550: =head1 Theory
! 551: The lond object is a state machine. It lives through the following states:
! 552:
! 553: =item Connected: a TCP connection has been formed, but the passkey has not yet
! 554: been negotiated.
! 555: =item Initialized: "init" sent.
! 556: =item ChallengeReceived: lond sent its challenge to us.
! 557: =item ChallengeReplied: We replied to lond's challenge waiting for lond's ok.
! 558: =item RequestingKey: We are requesting an encryption key.
! 559: =item ReceivingKey: We are receiving an encryption key.
! 560: =item Idle: Connection was negotiated but no requests are active.
! 561: =item SendingRequest: A request is being sent to the peer.
! 562: =item ReceivingReply: Waiting for an entire reply from the peer.
! 563: =item Disconnected: For whatever reason, the connection was dropped.
! 564:
! 565: When we need to be writing data, we have a writable
! 566: event. When we need to be reading data, a readable event established.
! 567: Events dispatch through the class functions Readable and Writable, and the
! 568: watcher contains a reference to the associated object to allow object context
! 569: to be reached.
! 570:
! 571: =head2 Member data.
! 572: Host - Host socket is connected to.
! 573: Port - The port the remote lond is listening on.
! 574: Socket - Socket open on the connection.
! 575: State - The current state.
! 576: TransactionRequest - The request being transmitted.
! 577: TransactionReply - The reply being received from the transaction.
! 578: InformReadable - True if we want to be called when socket is readable.
! 579: InformWritable - True if we want to be informed if the socket is writable.
! 580: Timeoutable - True if the current operation is allowed to timeout.
! 581: TimeoutValue - Number of seconds in the timeout.
! 582: TimeoutRemaining - Number of seconds left in the timeout.
! 583: CipherKey - The key that was negotiated with the peer.
! 584: Cipher - The cipher obtained via the key.
! 585:
! 586:
! 587:
! 588: =head2 The following are callback like members:
! 589: =item Tick: Called in response to a timer tick. Used to managed timeouts etc.
! 590: =item Readable: Called when the socket becomes readable.
! 591: =item Writable: Called when the socket becomes writable.
! 592: =item TimedOut: Called when a timed operation timed out.
! 593:
! 594: =head2 The following are operational member functions.
! 595: =item InitiateTransaction: Called to initiate a new transaction
! 596: =item SetStateTransitionCallback: Called to establish a function that is called
! 597: whenever the object goes through a state transition. This is used by
! 598: The client to manage the work flow for the object.
! 599: =item SetTimeoutCallback -Set a function to be called when a transaction times
! 600: out. The function will be called with the object as its sole parameter.
! 601: =item Encrypt - Encrypts a block of text according to the cipher negotiated
! 602: with the peer (assumes the text is a command).
! 603: =item Decrypt - Decrypts a block of text according to the cipher negotiated
! 604: with the peer (assumes the block was a reply.
! 605:
! 606: =head2 The following are selector member functions:
! 607:
! 608: =item GetState: Returns the current state
! 609: =item GetSocket: Gets the socekt open on the connection to lond.
! 610: =item WantReadable: true if the current state requires a readable event.
! 611: =item WantWritable: true if the current state requires a writable event.
! 612: =item WantTimeout: true if the current state requires timeout support.
! 613: =item GetHostIterator: Returns an iterator into the host file hash.
! 614: =cut
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>