Annotation of loncom/LondConnection.pm, revision 1.39
1.2 albertel 1: # This module defines and implements a class that represents
2: # a connection to a lond daemon.
3: #
1.38 albertel 4: # $Id: LondConnection.pm,v 1.37 2005/05/27 21:49:18 albertel Exp $
1.2 albertel 5: #
6: # Copyright Michigan State University Board of Trustees
7: #
8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
9: #
10: # LON-CAPA is free software; you can redistribute it and/or modify
11: # it under the terms of the GNU General Public License as published by
12: # the Free Software Foundation; either version 2 of the License, or
13: # (at your option) any later version.
14: #
15: # LON-CAPA is distributed in the hope that it will be useful,
16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18: # GNU General Public License for more details.
19: #
20: # You should have received a copy of the GNU General Public License
21: # along with LON-CAPA; if not, write to the Free Software
22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23: #
24: # /home/httpd/html/adm/gpl.txt
25: #
26: # http://www.lon-capa.org/
1.1 foxr 27: #
1.14 foxr 28:
1.1 foxr 29: package LondConnection;
30:
1.10 foxr 31: use strict;
1.1 foxr 32: use IO::Socket;
33: use IO::Socket::INET;
34: use IO::Handle;
35: use IO::File;
36: use Fcntl;
37: use POSIX;
38: use Crypt::IDEA;
1.31 foxr 39: use LONCAPA::lonlocal;
40: use LONCAPA::lonssl;
1.14 foxr 41:
1.1 foxr 42:
1.12 foxr 43:
44:
1.32 foxr 45: my $DebugLevel=0;
1.12 foxr 46: my %hostshash;
47: my %perlvar;
1.31 foxr 48: my $LocalDns = ""; # Need not be defined for managers.
49: my $InsecureOk;
1.1 foxr 50:
1.14 foxr 51: #
1.16 foxr 52: # Set debugging level
53: #
54: sub SetDebug {
55: $DebugLevel = shift;
56: }
57:
58: #
1.14 foxr 59: # The config read is done in this way to support the read of
60: # the non-default configuration file in the
61: # event we are being used outside of loncapa.
62: #
63:
64: my $ConfigRead = 0;
65:
1.1 foxr 66: # Read the configuration file for apache to get the perl
1.31 foxr 67: # variables set.
1.1 foxr 68:
1.12 foxr 69: sub ReadConfig {
1.31 foxr 70: Debug(8, "ReadConfig called");
71:
1.14 foxr 72: my $perlvarref = read_conf('loncapa.conf');
1.12 foxr 73: %perlvar = %{$perlvarref};
1.14 foxr 74: my $hoststab = read_hosts(
1.21 foxr 75: "$perlvar{lonTabDir}/hosts.tab") ||
1.14 foxr 76: die "Can't read host table!!";
1.12 foxr 77: %hostshash = %{$hoststab};
1.17 foxr 78: $ConfigRead = 1;
1.12 foxr 79:
1.31 foxr 80: my $myLonCapaName = $perlvar{lonHostID};
81: Debug(8, "My loncapa name is $myLonCapaName");
82:
83: if(defined $hostshash{$myLonCapaName}) {
84: Debug(8, "My loncapa name is in hosthash");
85: my @ConfigLine = @{$hostshash{$myLonCapaName}};
86: $LocalDns = $ConfigLine[3];
87: Debug(8, "Got local name $LocalDns");
88: }
89: $InsecureOk = $perlvar{loncAllowInsecure};
90:
91: Debug(3, "ReadConfig - LocalDNS = $LocalDns");
1.12 foxr 92: }
93:
1.15 foxr 94: #
95: # Read a foreign configuration.
96: # This sub is intended for the cases where the package
97: # will be read from outside the LonCAPA environment, in that case
98: # the client will need to explicitly provide:
99: # - A file in hosts.tab format.
100: # - Some idea of the 'lonCAPA' name of the local host (for building
101: # the encryption key).
102: #
103: # Parameters:
104: # MyHost - Name of this host as far as LonCAPA is concerned.
105: # Filename - Name of a hosts.tab formatted file that will be used
106: # to build up the hosts table.
107: #
108: sub ReadForeignConfig {
1.30 foxr 109:
110: my ($MyHost, $Filename) = @_;
1.15 foxr 111:
1.17 foxr 112: &Debug(4, "ReadForeignConfig $MyHost $Filename\n");
113:
1.15 foxr 114: $perlvar{lonHostID} = $MyHost; # Rmember my host.
115: my $hosttab = read_hosts($Filename) ||
116: die "Can't read hosts table!!";
1.17 foxr 117: %hostshash = %{$hosttab};
118: if($DebugLevel > 3) {
119: foreach my $host (keys %hostshash) {
1.31 foxr 120: print STDERR "host $host => $hostshash{$host}\n";
1.17 foxr 121: }
122: }
123: $ConfigRead = 1;
1.1 foxr 124:
1.31 foxr 125: my $myLonCapaName = $perlvar{lonHostID};
126:
127: if(defined $hostshash{$myLonCapaName}) {
128: my @ConfigLine = @{$hostshash{$myLonCapaName}};
129: $LocalDns = $ConfigLine[3];
130: }
131: $InsecureOk = $perlvar{loncAllowInsecure};
132:
133: Debug(3, "ReadForeignConfig - LocalDNS = $LocalDns");
134:
1.15 foxr 135: }
1.1 foxr 136:
137: sub Debug {
1.30 foxr 138:
139: my ($level, $message) = @_;
140:
1.1 foxr 141: if ($level < $DebugLevel) {
1.31 foxr 142: print STDERR ($message."\n");
1.1 foxr 143: }
144: }
1.3 albertel 145:
146: =pod
147:
148: =head2 Dump
149:
1.12 foxr 150: Dump the internal state of the object: For debugging purposes, to stderr.
1.3 albertel 151:
1.1 foxr 152: =cut
153:
154: sub Dump {
155: my $self = shift;
1.32 foxr 156: my $level = shift;
1.35 foxr 157: my $now = time;
158: my $local = localtime($now);
1.32 foxr 159:
1.37 albertel 160: if ($level >= $DebugLevel) {
1.32 foxr 161: return;
162: }
163:
1.35 foxr 164:
1.10 foxr 165: my $key;
166: my $value;
1.35 foxr 167: print STDERR "[ $local ] Dumping LondConnectionObject:\n";
1.37 albertel 168: print STDERR join(':',caller(1))."\n";
1.1 foxr 169: while(($key, $value) = each %$self) {
1.22 foxr 170: print STDERR "$key -> $value\n";
1.1 foxr 171: }
1.23 foxr 172: print STDERR "-------------------------------\n";
1.1 foxr 173: }
174:
175: =pod
1.3 albertel 176:
177: Local function to do a state transition. If the state transition
178: callback is defined it is called with two parameters: the self and the
179: old state.
180:
1.1 foxr 181: =cut
1.3 albertel 182:
1.1 foxr 183: sub Transition {
1.30 foxr 184:
185: my ($self, $newstate) = @_;
186:
1.1 foxr 187: my $oldstate = $self->{State};
188: $self->{State} = $newstate;
189: $self->{TimeoutRemaining} = $self->{TimeoutValue};
190: if($self->{TransitionCallback}) {
191: ($self->{TransitionCallback})->($self, $oldstate);
192: }
193: }
194:
1.3 albertel 195:
1.14 foxr 196:
1.1 foxr 197: =pod
1.3 albertel 198:
199: =head2 new
200:
201: Construct a new lond connection.
202:
203: Parameters (besides the class name) include:
204:
205: =item hostname
206:
207: host the remote lond is on. This host is a host in the hosts.tab file
208:
209: =item port
210:
211: port number the remote lond is listening on.
212:
1.1 foxr 213: =cut
1.3 albertel 214:
1.1 foxr 215: sub new {
1.39 ! albertel 216: my ($class, $DnsName, $Port) = @_;
1.14 foxr 217:
218: if (!$ConfigRead) {
219: ReadConfig();
220: $ConfigRead = 1;
221: }
1.39 ! albertel 222: &Debug(4,$class."::new( ".$DnsName.",".$Port.")\n");
1.1 foxr 223:
224: # The host must map to an entry in the hosts table:
225: # We connect to the dns host that corresponds to that
226: # system and use the hostname for the encryption key
227: # negotion. In the objec these become the Host and
228: # LoncapaHim fields of the object respectively.
229: #
1.39 ! albertel 230: if (!exists $hostshash{$DnsName}) {
! 231: &Debug(8, "No Such host $DnsName");
1.1 foxr 232: return undef; # No such host!!!
233: }
1.39 ! albertel 234: my @ConfigLine = @{$hostshash{$DnsName}};
! 235: my $Hostname = $ConfigLine[0]; # 0'th item is the msu id of host.
1.1 foxr 236: Debug(5, "Connecting to ".$DnsName);
1.36 albertel 237: # if it is me use loopback for connection
238: if ($DnsName eq $LocalDns) { $DnsName="127.0.0.1"; }
239: Debug(8, "Connecting to $DnsName I am $LocalDns");
1.1 foxr 240: # Now create the object...
241: my $self = { Host => $DnsName,
1.24 foxr 242: LoncapaHim => $Hostname,
243: Port => $Port,
244: State => "Initialized",
1.31 foxr 245: AuthenticationMode => "",
1.24 foxr 246: TransactionRequest => "",
247: TransactionReply => "",
1.39 ! albertel 248: NextRequest => "",
1.24 foxr 249: InformReadable => 0,
250: InformWritable => 0,
251: TimeoutCallback => undef,
252: TransitionCallback => undef,
253: Timeoutable => 0,
254: TimeoutValue => 30,
255: TimeoutRemaining => 0,
1.31 foxr 256: LocalKeyFile => "",
1.24 foxr 257: CipherKey => "",
258: LondVersion => "Unknown",
259: Cipher => undef};
1.1 foxr 260: bless($self, $class);
261: unless ($self->{Socket} = IO::Socket::INET->new(PeerHost => $self->{Host},
1.27 foxr 262: PeerPort => $self->{Port},
263: Type => SOCK_STREAM,
264: Proto => "tcp",
265: Timeout => 3)) {
1.36 albertel 266: Debug(8, "Error? \n$@ \n$!");
1.1 foxr 267: return undef; # Inidicates the socket could not be made.
268: }
1.31 foxr 269: my $socket = $self->{Socket}; # For local use only.
1.33 foxr 270: # If we are local, we'll first try local auth mode, otherwise, we'll try
271: # the ssl auth mode:
1.31 foxr 272:
273: my $key;
274: my $keyfile;
1.36 albertel 275: if ($DnsName eq '127.0.0.1') {
1.31 foxr 276: $self->{AuthenticationMode} = "local";
277: ($key, $keyfile) = lonlocal::CreateKeyFile();
278: Debug(8, "Local key: $key, stored in $keyfile");
279:
280: # If I can't make the key file fall back to insecure if
281: # allowed...else give up right away.
282:
283: if(!(defined $key) || !(defined $keyfile)) {
284: if($InsecureOk) {
285: $self->{AuthenticationMode} = "insecure";
286: $self->{TransactionRequest} = "init\n";
287: }
288: else {
289: $socket->close;
290: return undef;
291: }
292: }
293: $self->{TransactionRequest} = "init:local:$keyfile\n";
294: Debug(9, "Init string is init:local:$keyfile");
295: if(!$self->CreateCipher($key)) { # Nothing's going our way...
296: $socket->close;
297: return undef;
298: }
299:
1.33 foxr 300: }
1.31 foxr 301: else {
1.33 foxr 302: # Remote peer: I'd like to do ssl, but if my host key or certificates
303: # are not all installed, my only choice is insecure, if that's
304: # allowed:
305:
306: my ($ca, $cert) = lonssl::CertificateFile;
307: my $sslkeyfile = lonssl::KeyFile;
308:
309: if((defined $ca) && (defined $cert) && (defined $sslkeyfile)) {
310:
311: $self->{AuthenticationMode} = "ssl";
312: $self->{TransactionRequest} = "init:ssl\n";
313: } else {
314: if($InsecureOk) { # Allowed to do insecure:
315: $self->{AuthenticationMode} = "insecure";
316: $self->{TransactionRequest} = "init\n";
317: }
318: else { # Not allowed to do insecure...
319: $socket->close;
320: return undef;
321: }
322: }
1.31 foxr 323: }
324:
1.1 foxr 325: #
326: # We're connected. Set the state, and the events we'll accept:
327: #
328: $self->Transition("Connected");
329: $self->{InformWritable} = 1; # When socket is writable we send init
1.9 foxr 330: $self->{Timeoutable} = 1; # Timeout allowed during startup negotiation.
1.31 foxr 331:
1.1 foxr 332:
333: #
334: # Set socket to nonblocking I/O.
335: #
336: my $socket = $self->{Socket};
1.31 foxr 337: my $flags = fcntl($socket, F_GETFL,0);
338: if(!$flags) {
1.1 foxr 339: $socket->close;
340: return undef;
341: }
1.31 foxr 342: if(!fcntl($socket, F_SETFL, $flags | O_NONBLOCK)) {
1.1 foxr 343: $socket->close;
344: return undef;
345: }
346:
347: # return the object :
348:
1.31 foxr 349: Debug(9, "Initial object state: ");
1.32 foxr 350: $self->Dump(9);
1.31 foxr 351:
1.1 foxr 352: return $self;
353: }
1.3 albertel 354:
1.1 foxr 355: =pod
1.3 albertel 356:
357: =head2 Readable
358:
359: This member should be called when the Socket becomes readable. Until
360: the read completes, action is state independet. Data are accepted into
361: the TransactionReply until a newline character is received. At that
362: time actionis state dependent:
363:
364: =item Connected
365:
366: in this case we received challenge, the state changes to
367: ChallengeReceived, and we initiate a send with the challenge response.
368:
369: =item ReceivingReply
370:
371: In this case a reply has been received for a transaction, the state
372: goes to Idle and we disable write and read notification.
373:
374: =item ChallengeReeived
375:
376: we just got what should be an ok\n and the connection can now handle
377: transactions.
1.1 foxr 378:
379: =cut
1.3 albertel 380:
1.1 foxr 381: sub Readable {
382: my $self = shift;
383: my $socket = $self->{Socket};
384: my $data = '';
1.27 foxr 385: my $rv;
1.31 foxr 386: my $ConnectionMode = $self->{AuthenticationMode};
387:
1.27 foxr 388: if ($socket) {
389: eval {
390: $rv = $socket->recv($data, POSIX::BUFSIZ, 0);
391: }
392: } else {
393: $self->Transition("Disconnected");
394: return -1;
395: }
1.1 foxr 396: my $errno = $! + 0; # Force numeric context.
397:
1.8 foxr 398: unless (defined($rv) && length $data) {# Read failed,
1.1 foxr 399: if(($errno == POSIX::EWOULDBLOCK) ||
400: ($errno == POSIX::EAGAIN) ||
1.8 foxr 401: ($errno == POSIX::EINTR)) {
1.1 foxr 402: return 0;
403: }
404:
405: # Connection likely lost.
406: &Debug(4, "Connection lost");
407: $self->{TransactionRequest} = '';
408: $socket->close();
409: $self->Transition("Disconnected");
410: return -1;
411: }
412: # Append the data to the buffer. And figure out if the read is done:
413:
414: &Debug(9,"Received from host: ".$data);
415: $self->{TransactionReply} .= $data;
1.29 albertel 416: if($self->{TransactionReply} =~ m/\n$/) {
1.1 foxr 417: &Debug(8,"Readable End of line detected");
1.31 foxr 418:
419:
1.1 foxr 420: if ($self->{State} eq "Initialized") { # We received the challenge:
1.31 foxr 421: # Our init was replied to. What happens next depends both on
422: # the actual init we sent (AuthenticationMode member data)
423: # and the response:
424: # AuthenticationMode == local:
425: # Response ok: The key has been exchanged and
426: # the key file destroyed. We can jump
427: # into setting the host and requesting the
428: # Later we'll also bypass key exchange.
429: # Response digits:
430: # Old style lond. Delete the keyfile.
431: # If allowed fall back to insecure mode.
432: # else close connection and fail.
433: # Response other:
434: # Failed local auth
435: # Close connection and fail.
436: #
437: # AuthenticationMode == ssl:
438: # Response ok:ssl
439: # Response digits:
440: # Response other:
441: # Authentication mode == insecure
442: # Response digits
443: # Response other:
444:
445: my $Response = $self->{TransactionReply};
446: if($ConnectionMode eq "local") {
447: if($Response =~ /^ok:local/) { # Good local auth.
448: $self->ToVersionRequest();
449: return 0;
450: }
451: elsif ($Response =~/^[0-9]+/) { # Old style lond.
452: return $self->CompleteInsecure();
453:
454: }
455: else { # Complete flop
456: &Debug(3, "init:local : unrecognized reply");
457: $self->Transition("Disconnected");
458: $socket->close;
459: return -1;
460: }
461: }
462: elsif ($ConnectionMode eq "ssl") {
463: if($Response =~ /^ok:ssl/) { # Good ssl...
464: if($self->ExchangeKeysViaSSL()) { # Success skip to vsn stuff
465: # Need to reset to non blocking:
466:
467: my $flags = fcntl($socket, F_GETFL, 0);
468: fcntl($socket, F_SETFL, $flags | O_NONBLOCK);
469: $self->ToVersionRequest();
470: return 0;
471: }
472: else { # Failed in ssl exchange.
473: &Debug(3,"init:ssl failed key negotiation!");
474: $self->Transition("Disconnected");
475: $socket->close;
476: return -1;
477: }
478: }
479: elsif ($Response =~ /^[0-9]+/) { # Old style lond.
480: return $self->CompleteInsecure();
481: }
482: else { # Complete flop
483: }
484: }
485: elsif ($ConnectionMode eq "insecure") {
486: if($self->{TransactionReply} eq "refused\n") { # Remote doesn't have
487:
488: $self->Transition("Disconnected"); # in host tables.
489: $socket->close();
490: return -1;
491:
492: }
493: return $self->CompleteInsecure();
494: }
495: else {
496: &Debug(1,"Authentication mode incorrect");
497: die "BUG!!! LondConnection::Readable invalid authmode";
1.1 foxr 498: }
1.27 foxr 499:
1.31 foxr 500:
1.28 albertel 501: } elsif ($self->{State} eq "ChallengeReplied") {
502: if($self->{TransactionReply} ne "ok\n") {
503: $self->Transition("Disconnected");
504: $socket->close();
505: return -1;
506: }
1.31 foxr 507: $self->ToVersionRequest();
1.28 albertel 508: return 0;
1.31 foxr 509:
1.28 albertel 510: } elsif ($self->{State} eq "ReadingVersionString") {
1.38 albertel 511: chomp($self->{TransactionReply});
512: $self->{LondVersion} = $self->{TransactionReply};
1.28 albertel 513: $self->Transition("SetHost");
514: $self->{InformReadable} = 0;
515: $self->{InformWritable} = 1;
516: my $peer = $self->{LoncapaHim};
517: $self->{TransactionRequest}= "sethost:$peer\n";
518: return 0;
1.24 foxr 519: } elsif ($self->{State} eq "HostSet") { # should be ok.
1.28 albertel 520: if($self->{TransactionReply} ne "ok\n") {
521: $self->Transition("Disconnected");
522: $socket->close();
523: return -1;
524: }
1.31 foxr 525: # If the auth mode is insecure we must still
526: # exchange session keys. Otherwise,
527: # we can just transition to idle.
528:
529: if($ConnectionMode eq "insecure") {
530: $self->Transition("RequestingKey");
531: $self->{InformReadable} = 0;
532: $self->{InformWritable} = 1;
533: $self->{TransactionRequest} = "ekey\n";
534: return 0;
535: }
536: else {
537: $self->ToIdle();
538: return 0;
539: }
1.1 foxr 540: } elsif ($self->{State} eq "ReceivingKey") {
541: my $buildkey = $self->{TransactionReply};
542: my $key = $self->{LoncapaHim}.$perlvar{'lonHostID'};
543: $key=~tr/a-z/A-Z/;
544: $key=~tr/G-P/0-9/;
545: $key=~tr/Q-Z/0-9/;
1.31 foxr 546: $key =$key.$buildkey.$key.$buildkey.$key.$buildkey;
547: $key = substr($key,0,32);
548: if(!$self->CreateCipher($key)) {
1.1 foxr 549: $self->Transition("Disconnected");
550: $socket->close();
551: return -1;
552: } else {
1.31 foxr 553: $self->ToIdle();
1.1 foxr 554: return 0;
555: }
556: } elsif ($self->{State} eq "ReceivingReply") {
557:
558: # If the data are encrypted, decrypt first.
559:
560: my $answer = $self->{TransactionReply};
561: if($answer =~ /^enc\:/) {
562: $answer = $self->Decrypt($answer);
1.34 foxr 563: $self->{TransactionReply} = "$answer\n";
1.1 foxr 564: }
1.39 ! albertel 565: # if we have a NextRequest do it immeadiately
! 566: if ($self->{NextRequest}) {
! 567: $self->{TransactionRequest} = $self->{NextRequest};
! 568: undef( $self->{NextRequest} );
! 569: $self->{TransactionReply} = "";
! 570: $self->{InformWritable} = 1;
! 571: $self->{InformReadable} = 0;
! 572: $self->{Timeoutable} = 1;
! 573: $self->{TimeoutRemaining} = $self->{TimeoutValue};
! 574: $self->Transition("SendingRequest");
! 575: return 0;
! 576: } else {
1.1 foxr 577: # finish the transaction
578:
1.39 ! albertel 579: $self->ToIdle();
! 580: return 0;
! 581: }
1.1 foxr 582: } elsif ($self->{State} eq "Disconnected") { # No connection.
583: return -1;
584: } else { # Internal error: Invalid state.
585: $self->Transition("Disconnected");
586: $socket->close();
587: return -1;
588: }
589: }
590:
591: return 0;
1.27 foxr 592:
1.1 foxr 593: }
594:
595:
596: =pod
1.3 albertel 597:
598: This member should be called when the Socket becomes writable.
599:
600: The action is state independent. An attempt is made to drain the
601: contents of the TransactionRequest member. Once this is drained, we
602: mark the object as waiting for readability.
1.1 foxr 603:
604: Returns 0 if successful, or -1 if not.
1.3 albertel 605:
1.1 foxr 606: =cut
607: sub Writable {
608: my $self = shift; # Get reference to the object.
609: my $socket = $self->{Socket};
1.26 albertel 610: my $nwritten;
611: if ($socket) {
612: eval {
613: $nwritten = $socket->send($self->{TransactionRequest}, 0);
614: }
1.27 foxr 615: } else {
616: # For whatever reason, there's no longer a socket left.
617:
618:
619: $self->Transition("Disconnected");
620: return -1;
1.26 albertel 621: }
1.1 foxr 622: my $errno = $! + 0;
623: unless (defined $nwritten) {
624: if($errno != POSIX::EINTR) {
625: $self->Transition("Disconnected");
626: return -1;
627: }
628:
629: }
1.10 foxr 630: if (($nwritten >= 0) ||
1.1 foxr 631: ($errno == POSIX::EWOULDBLOCK) ||
632: ($errno == POSIX::EAGAIN) ||
633: ($errno == POSIX::EINTR) ||
634: ($errno == 0)) {
635: substr($self->{TransactionRequest}, 0, $nwritten) = ""; # rmv written part
1.27 foxr 636: if(length $self->{TransactionRequest} == 0) {
637: $self->{InformWritable} = 0;
638: $self->{InformReadable} = 1;
639: $self->{TransactionReply} = '';
640: #
641: # Figure out the next state:
642: #
643: if($self->{State} eq "Connected") {
644: $self->Transition("Initialized");
645: } elsif($self->{State} eq "ChallengeReceived") {
646: $self->Transition("ChallengeReplied");
647: } elsif($self->{State} eq "RequestingVersion") {
648: $self->Transition("ReadingVersionString");
649: } elsif ($self->{State} eq "SetHost") {
650: $self->Transition("HostSet");
651: } elsif($self->{State} eq "RequestingKey") {
652: $self->Transition("ReceivingKey");
1.24 foxr 653: # $self->{InformWritable} = 0;
654: # $self->{InformReadable} = 1;
655: # $self->{TransactionReply} = '';
1.27 foxr 656: } elsif ($self->{State} eq "SendingRequest") {
657: $self->Transition("ReceivingReply");
658: $self->{TimeoutRemaining} = $self->{TimeoutValue};
659: } elsif ($self->{State} eq "Disconnected") {
660: return -1;
661: }
662: return 0;
663: }
664: } else { # The write failed (e.g. partner disconnected).
665: $self->Transition("Disconnected");
666: $socket->close();
667: return -1;
668: }
669:
1.1 foxr 670: }
671: =pod
1.3 albertel 672:
673: =head2 Tick
674:
1.1 foxr 675: Tick is called every time unit by the event framework. It
1.3 albertel 676:
677: =item 1 decrements the remaining timeout.
678:
679: =item 2 If the timeout is zero, calls TimedOut indicating that the current operation timed out.
1.1 foxr 680:
681: =cut
682:
683: sub Tick {
684: my $self = shift;
685: $self->{TimeoutRemaining}--;
686: if ($self->{TimeoutRemaining} < 0) {
687: $self->TimedOut();
688: }
689: }
1.3 albertel 690:
1.1 foxr 691: =pod
692:
1.3 albertel 693: =head2 TimedOut
694:
695: called on a timeout. If the timeout callback is defined, it is called
696: with $self as its parameters.
697:
698: =cut
699:
1.1 foxr 700: sub TimedOut {
701:
702: my $self = shift;
703: if($self->{TimeoutCallback}) {
704: my $callback = $self->{TimeoutCallback};
705: my @args = ( $self);
706: &$callback(@args);
707: }
708: }
1.3 albertel 709:
1.1 foxr 710: =pod
1.3 albertel 711:
712: =head2 InitiateTransaction
713:
714: Called to initiate a transaction. A transaction can only be initiated
715: when the object is idle... otherwise an error is returned. A
716: transaction consists of a request to the server that will have a
717: reply. This member sets the request data in the TransactionRequest
718: member, makes the state SendingRequest and sets the data to allow a
719: timout, and to request writability notification.
720:
1.1 foxr 721: =cut
1.3 albertel 722:
1.1 foxr 723: sub InitiateTransaction {
1.30 foxr 724:
725: my ($self, $data) = @_;
1.1 foxr 726:
1.4 foxr 727: Debug(1, "initiating transaction: ".$data);
1.1 foxr 728: if($self->{State} ne "Idle") {
1.4 foxr 729: Debug(0," .. but not idle here\n");
1.1 foxr 730: return -1; # Error indicator.
731: }
732: # if the transaction is to be encrypted encrypt the data:
1.39 ! albertel 733: (my $sethost, my $server,$data)=split(/:/,$data,3);
1.1 foxr 734:
735: if($data =~ /^encrypt\:/) {
736: $data = $self->Encrypt($data);
737: }
738:
739: # Setup the trasaction
1.39 ! albertel 740: # currently no version of lond supports inlining the sethost
! 741: if ($self->PeerVersion() <= 10000000) {
! 742: if ($server ne $self->{LoncapaHim}) {
! 743: $self->{NextRequest} = $data;
! 744: $self->{TransactionRequest} = "$sethost:$server\n";
! 745: $self->{LoncapaHim} = $server;
! 746: } else {
! 747: $self->{TransactionRequest} = $data;
! 748: }
! 749: } else {
! 750: $self->{TransactionRequest} = "$sethost:$server:$data";
! 751: }
1.1 foxr 752: $self->{TransactionReply} = "";
753: $self->{InformWritable} = 1;
754: $self->{InformReadable} = 0;
755: $self->{Timeoutable} = 1;
756: $self->{TimeoutRemaining} = $self->{TimeoutValue};
757: $self->Transition("SendingRequest");
758: }
759:
760:
761: =pod
1.3 albertel 762:
763: =head2 SetStateTransitionCallback
764:
765: Sets a callback for state transitions. Returns a reference to any
766: prior established callback, or undef if there was none:
767:
1.1 foxr 768: =cut
1.3 albertel 769:
1.1 foxr 770: sub SetStateTransitionCallback {
771: my $self = shift;
772: my $oldCallback = $self->{TransitionCallback};
773: $self->{TransitionCallback} = shift;
774: return $oldCallback;
775: }
1.3 albertel 776:
1.1 foxr 777: =pod
1.3 albertel 778:
779: =head2 SetTimeoutCallback
780:
781: Sets the timeout callback. Returns a reference to any prior
782: established callback or undef if there was none.
783:
1.1 foxr 784: =cut
1.3 albertel 785:
1.1 foxr 786: sub SetTimeoutCallback {
1.30 foxr 787:
788: my ($self, $callback) = @_;
789:
1.1 foxr 790: my $oldCallback = $self->{TimeoutCallback};
791: $self->{TimeoutCallback} = $callback;
792: return $oldCallback;
793: }
794:
795: =pod
1.3 albertel 796:
1.5 foxr 797: =head2 Shutdown:
798:
799: Shuts down the socket.
800:
801: =cut
802:
803: sub Shutdown {
804: my $self = shift;
805: my $socket = $self->GetSocket();
1.20 albertel 806: Debug(5,"socket is -$socket-");
807: if ($socket) {
808: # Ask lond to exit too. Non blocking so
809: # there is no cost for failure.
810: eval {
811: $socket->send("exit\n", 0);
812: $socket->shutdown(2);
813: }
814: }
1.5 foxr 815: }
816:
817: =pod
818:
1.3 albertel 819: =head2 GetState
820:
821: selector for the object state.
822:
1.1 foxr 823: =cut
1.3 albertel 824:
1.1 foxr 825: sub GetState {
826: my $self = shift;
827: return $self->{State};
828: }
1.3 albertel 829:
1.1 foxr 830: =pod
1.3 albertel 831:
832: =head2 GetSocket
833:
834: selector for the object socket.
835:
1.1 foxr 836: =cut
1.3 albertel 837:
1.1 foxr 838: sub GetSocket {
839: my $self = shift;
840: return $self->{Socket};
841: }
1.3 albertel 842:
1.5 foxr 843:
1.1 foxr 844: =pod
1.3 albertel 845:
846: =head2 WantReadable
847:
848: Return the state of the flag that indicates the object wants to be
849: called when readable.
850:
1.1 foxr 851: =cut
1.3 albertel 852:
1.1 foxr 853: sub WantReadable {
854: my $self = shift;
855:
856: return $self->{InformReadable};
857: }
1.3 albertel 858:
1.1 foxr 859: =pod
1.3 albertel 860:
861: =head2 WantWritable
862:
863: Return the state of the flag that indicates the object wants write
864: notification.
865:
1.1 foxr 866: =cut
1.3 albertel 867:
1.1 foxr 868: sub WantWritable {
869: my $self = shift;
870: return $self->{InformWritable};
871: }
1.3 albertel 872:
1.1 foxr 873: =pod
1.3 albertel 874:
875: =head2 WantTimeout
876:
877: return the state of the flag that indicates the object wants to be
878: informed of timeouts.
879:
1.1 foxr 880: =cut
1.3 albertel 881:
1.1 foxr 882: sub WantTimeout {
883: my $self = shift;
884: return $self->{Timeoutable};
885: }
886:
887: =pod
1.3 albertel 888:
889: =head2 GetReply
890:
891: Returns the reply from the last transaction.
892:
1.1 foxr 893: =cut
1.3 albertel 894:
1.1 foxr 895: sub GetReply {
896: my $self = shift;
897: return $self->{TransactionReply};
898: }
899:
900: =pod
1.3 albertel 901:
902: =head2 Encrypt
903:
904: Returns the encrypted version of the command string.
905:
906: The command input string is of the form:
907:
1.1 foxr 908: encrypt:command
1.3 albertel 909:
910: The output string can be directly sent to lond as it is of the form:
911:
1.1 foxr 912: enc:length:<encodedrequest>
1.3 albertel 913:
1.1 foxr 914: =cut
1.3 albertel 915:
1.1 foxr 916: sub Encrypt {
1.30 foxr 917:
918: my ($self, $request) = @_;
1.1 foxr 919:
920:
921: # Split the encrypt: off the request and figure out it's length.
922: # the cipher works in blocks of 8 bytes.
923:
924: my $cmd = $request;
925: $cmd =~ s/^encrypt\://; # strip off encrypt:
926: chomp($cmd); # strip off trailing \n
927: my $length=length($cmd); # Get the string length.
928: $cmd .= " "; # Pad with blanks so we can fill out a block.
929:
930: # encrypt the request in 8 byte chunks to create the encrypted
931: # output request.
932:
933: my $Encoded = '';
934: for(my $index = 0; $index <= $length; $index += 8) {
935: $Encoded .=
936: unpack("H16",
937: $self->{Cipher}->encrypt(substr($cmd,
938: $index, 8)));
939: }
940:
941: # Build up the answer as enc:length:$encrequest.
942:
943: $request = "enc:$length:$Encoded\n";
944: return $request;
945:
946:
947: }
1.3 albertel 948:
949: =pod
950:
951: =head2 Decrypt
952:
953: Decrypt a response from the server. The response is in the form:
954:
955: enc:<length>:<encrypted data>
956:
1.1 foxr 957: =cut
1.3 albertel 958:
1.1 foxr 959: sub Decrypt {
1.30 foxr 960:
961: my ($self, $encrypted) = @_;
1.1 foxr 962:
963: # Bust up the response into length, and encryptedstring:
964:
965: my ($enc, $length, $EncryptedString) = split(/:/,$encrypted);
966: chomp($EncryptedString);
967:
968: # Decode the data in 8 byte blocks. The string is encoded
969: # as hex digits so there are two characters per byte:
970:
1.10 foxr 971: my $decrypted = "";
1.1 foxr 972: for(my $index = 0; $index < length($EncryptedString);
973: $index += 16) {
974: $decrypted .= $self->{Cipher}->decrypt(
975: pack("H16",
976: substr($EncryptedString,
977: $index,
978: 16)));
979: }
980: # the answer may have trailing pads to fill out a block.
981: # $length tells us the actual length of the decrypted string:
982:
983: $decrypted = substr($decrypted, 0, $length);
1.34 foxr 984: Debug(9, "Decrypted $EncryptedString to $decrypted");
1.1 foxr 985:
986: return $decrypted;
987:
988: }
1.31 foxr 989: # ToIdle
990: # Called to transition to idle... done enough it's worth subbing
991: # off to ensure it's always done right!!
992: #
993: sub ToIdle {
994: my $self = shift;
995:
996: $self->Transition("Idle");
997: $self->{InformWritiable} = 0;
998: $self->{InformReadable} = 0;
999: $self->{Timeoutable} = 0;
1000: }
1001:
1002: # ToVersionRequest
1003: # Called to transition to "RequestVersion" also done a few times
1004: # so worth subbing out.
1005: #
1006: sub ToVersionRequest {
1007: my $self = shift;
1008:
1009: $self->Transition("RequestingVersion");
1010: $self->{InformReadable} = 0;
1011: $self->{InformWritable} = 1;
1012: $self->{TransactionRequest} = "version\n";
1013:
1014: }
1015: #
1016: # CreateCipher
1017: # Given a cipher key stores the key in the object context,
1018: # creates the cipher object, (stores that in object context),
1019: # This is done a couple of places, so it's worth factoring it out.
1020: #
1021: # Parameters:
1022: # (self)
1023: # key - The Cipher key.
1024: #
1025: # Returns:
1026: # 0 - Failure to create IDEA cipher.
1027: # 1 - Success.
1028: #
1029: sub CreateCipher {
1030: my ($self, $key) = @_; # According to coding std.
1031:
1032: $self->{CipherKey} = $key; # Save the text key...
1033: my $packedkey = pack ("H32", $key);
1034: my $cipher = new IDEA $packedkey;
1035: if($cipher) {
1036: $self->{Cipher} = $cipher;
1037: Debug("Cipher created dumping socket: ");
1.32 foxr 1038: $self->Dump(9);
1.31 foxr 1039: return 1;
1040: }
1041: else {
1042: return 0;
1043: }
1044: }
1045: # ExchangeKeysViaSSL
1046: # Called to do cipher key exchange via SSL.
1047: # The socket is promoted to an SSL socket. If that's successful,
1048: # we read out cipher key through the socket and create an IDEA
1049: # cipher object.
1050: # Parameters:
1051: # (self)
1052: # Returns:
1053: # true - Success.
1054: # false - Failure.
1055: #
1056: # Assumptions:
1057: # 1. The ssl session setup has timeout logic built in so we don't
1058: # have to worry about DOS attacks at that stage.
1059: # 2. If the ssl session gets set up we are talking to a legitimate
1060: # lond so again we don't have to worry about DOS attacks.
1061: # All this allows us just to call
1062: sub ExchangeKeysViaSSL {
1063: my $self = shift;
1064: my $socket = $self->{Socket};
1065:
1066: # Get our signed certificate, the certificate authority's
1067: # certificate and our private key file. All of these
1068: # are needed to create the ssl connection.
1069:
1070: my ($SSLCACertificate,
1071: $SSLCertificate) = lonssl::CertificateFile();
1072: my $SSLKey = lonssl::KeyFile();
1073:
1074: # Promote our connection to ssl and read the key from lond.
1075:
1076: my $SSLSocket = lonssl::PromoteClientSocket($socket,
1077: $SSLCACertificate,
1078: $SSLCertificate,
1079: $SSLKey);
1080: if(defined $SSLSocket) {
1081: my $key = <$SSLSocket>;
1082: lonssl::Close($SSLSocket);
1083: if($key) {
1084: chomp($key); # \n is not part of the key.
1085: return $self->CreateCipher($key);
1086: }
1087: else {
1088: Debug(3, "Failed to read ssl key");
1089: return 0;
1090: }
1091: }
1092: else {
1093: # Failed!!
1094: Debug(3, "Failed to negotiate SSL connection!");
1095: return 0;
1096: }
1097: # should not get here
1098: return 0;
1099:
1100: }
1101:
1102:
1103:
1104: #
1105: # CompleteInsecure:
1106: # This function is called to initiate the completion of
1107: # insecure challenge response negotiation.
1108: # To do this, we copy the challenge string to the transaction
1109: # request, flip to writability and state transition to
1110: # ChallengeReceived..
1111: # All this is only possible if InsecureOk is true.
1112: # Parameters:
1113: # (self) - This object's context hash.
1114: # Return:
1115: # 0 - Ok to transition.
1116: # -1 - Not ok to transition (InsecureOk not ok).
1117: #
1118: sub CompleteInsecure {
1119: my $self = shift;
1120: if($InsecureOk) {
1121: $self->{AuthenticationMode} = "insecure";
1122: &Debug(8," Transition out of Initialized:insecure");
1123: $self->{TransactionRequest} = $self->{TransactionReply};
1124: $self->{InformWritable} = 1;
1125: $self->{InformReadable} = 0;
1126: $self->Transition("ChallengeReceived");
1127: $self->{TimeoutRemaining} = $self->{TimeoutValue};
1128: return 0;
1129:
1130:
1131: }
1132: else {
1133: &Debug(3, "Insecure key negotiation disabled!");
1134: my $socket = $self->{Socket};
1135: $socket->close;
1136: return -1;
1137: }
1138: }
1.1 foxr 1139:
1140: =pod
1.3 albertel 1141:
1142: =head2 GetHostIterator
1.1 foxr 1143:
1144: Returns a hash iterator to the host information. Each get from
1145: this iterator returns a reference to an array that contains
1146: information read from the hosts configuration file. Array elements
1147: are used as follows:
1148:
1.3 albertel 1149: [0] - LonCapa host name.
1150: [1] - LonCapa domain name.
1151: [2] - Loncapa role (e.g. library or access).
1152: [3] - DNS name server hostname.
1.11 foxr 1153: [4] - IP address (result of e.g. nslookup [3]).
1.3 albertel 1154: [5] - Maximum connection count.
1155: [6] - Idle timeout for reducing connection count.
1156: [7] - Minimum connection count.
1.1 foxr 1157:
1.3 albertel 1158: =cut
1.1 foxr 1159:
1160: sub GetHostIterator {
1161:
1162: return HashIterator->new(\%hostshash);
1163: }
1.14 foxr 1164:
1165: ###########################################################
1166: #
1167: # The following is an unashamed kludge that is here to
1168: # allow LondConnection to be used outside of the
1169: # loncapa environment (e.g. by lonManage).
1170: #
1171: # This is a textual inclusion of pieces of the
1172: # Configuration.pm module.
1173: #
1174:
1175:
1176: my $confdir='/etc/httpd/conf/';
1177:
1178: # ------------------- Subroutine read_conf: read LON-CAPA server configuration.
1179: # This subroutine reads PerlSetVar values out of specified web server
1180: # configuration files.
1181: sub read_conf
1182: {
1183: my (@conf_files)=@_;
1184: my %perlvar;
1185: foreach my $filename (@conf_files,'loncapa_apache.conf')
1186: {
1.21 foxr 1187: if($DebugLevel > 3) {
1.31 foxr 1188: print STDERR ("Going to read $confdir.$filename\n");
1.21 foxr 1189: }
1.14 foxr 1190: open(CONFIG,'<'.$confdir.$filename) or
1191: die("Can't read $confdir$filename");
1192: while (my $configline=<CONFIG>)
1193: {
1194: if ($configline =~ /^[^\#]*PerlSetVar/)
1195: {
1196: my ($unused,$varname,$varvalue)=split(/\s+/,$configline);
1197: chomp($varvalue);
1198: $perlvar{$varname}=$varvalue;
1199: }
1200: }
1201: close(CONFIG);
1202: }
1.21 foxr 1203: if($DebugLevel > 3) {
1.31 foxr 1204: print STDERR "Dumping perlvar:\n";
1.21 foxr 1205: foreach my $var (keys %perlvar) {
1.31 foxr 1206: print STDERR "$var = $perlvar{$var}\n";
1.21 foxr 1207: }
1208: }
1.14 foxr 1209: my $perlvarref=\%perlvar;
1.21 foxr 1210: return $perlvarref;
1211: }
1.14 foxr 1212:
1213: #---------------------- Subroutine read_hosts: Read a LON-CAPA hosts.tab
1214: # formatted configuration file.
1215: #
1.36 albertel 1216: my $RequiredCount = 4; # Required item count in hosts.tab.
1.14 foxr 1217: my $DefaultMaxCon = 5; # Default value for maximum connections.
1218: my $DefaultIdle = 1000; # Default connection idle time in seconds.
1219: my $DefaultMinCon = 0; # Default value for minimum connections.
1220:
1221: sub read_hosts {
1222: my $Filename = shift;
1223: my %HostsTab;
1224:
1.36 albertel 1225: open(CONFIG,'<'.$Filename) or die("Can't read $Filename");
1.14 foxr 1226: while (my $line = <CONFIG>) {
1.36 albertel 1227: if ($line !~ /^\s*\#/) {
1228: $line=~s/\s*$//;
1.14 foxr 1229: my @items = split(/:/, $line);
1230: if(scalar @items >= $RequiredCount) {
1231: if (scalar @items == $RequiredCount) { # Only required items:
1232: $items[$RequiredCount] = $DefaultMaxCon;
1233: }
1234: if(scalar @items == $RequiredCount + 1) { # up through maxcon.
1235: $items[$RequiredCount+1] = $DefaultIdle;
1236: }
1237: if(scalar @items == $RequiredCount + 2) { # up through idle.
1238: $items[$RequiredCount+2] = $DefaultMinCon;
1239: }
1240: {
1241: my @list = @items; # probably not needed but I'm unsure of
1242: # about the scope of item so...
1.39 ! albertel 1243: $HostsTab{$list[3]} = \@list;
1.14 foxr 1244: }
1245: }
1246: }
1247: }
1248: close(CONFIG);
1249: my $hostref = \%HostsTab;
1250: return ($hostref);
1251: }
1.24 foxr 1252: #
1253: # Get the version of our peer. Note that this is only well
1254: # defined if the state machine has hit the idle state at least
1255: # once (well actually if it has transitioned out of
1256: # ReadingVersionString The member data LondVersion is returned.
1257: #
1258: sub PeerVersion {
1259: my $self = shift;
1.39 ! albertel 1260: my ($version) = ($self->{LondVersion} =~ /Revision 1\.(\d+)/);
1.24 foxr 1261: return $self->{LondVersion};
1262: }
1.1 foxr 1263:
1264: 1;
1265:
1266: =pod
1.3 albertel 1267:
1.1 foxr 1268: =head1 Theory
1269:
1.3 albertel 1270: The lond object is a state machine. It lives through the following states:
1271:
1272: =item Connected:
1273:
1274: a TCP connection has been formed, but the passkey has not yet been
1275: negotiated.
1276:
1277: =item Initialized:
1278:
1279: "init" sent.
1280:
1281: =item ChallengeReceived:
1282:
1283: lond sent its challenge to us.
1284:
1285: =item ChallengeReplied:
1286:
1287: We replied to lond's challenge waiting for lond's ok.
1288:
1289: =item RequestingKey:
1290:
1291: We are requesting an encryption key.
1292:
1293: =item ReceivingKey:
1294:
1295: We are receiving an encryption key.
1296:
1297: =item Idle:
1298:
1299: Connection was negotiated but no requests are active.
1300:
1301: =item SendingRequest:
1302:
1303: A request is being sent to the peer.
1304:
1305: =item ReceivingReply:
1306:
1307: Waiting for an entire reply from the peer.
1308:
1309: =item Disconnected:
1310:
1311: For whatever reason, the connection was dropped.
1312:
1313: When we need to be writing data, we have a writable event. When we
1314: need to be reading data, a readable event established. Events
1315: dispatch through the class functions Readable and Writable, and the
1316: watcher contains a reference to the associated object to allow object
1317: context to be reached.
1.1 foxr 1318:
1319: =head2 Member data.
1320:
1.3 albertel 1321: =item Host
1322:
1323: Host socket is connected to.
1324:
1325: =item Port
1326:
1327: The port the remote lond is listening on.
1328:
1329: =item Socket
1330:
1331: Socket open on the connection.
1332:
1333: =item State
1334:
1335: The current state.
1336:
1.31 foxr 1337: =item AuthenticationMode
1338:
1339: How authentication is being done. This can be any of:
1340:
1341: o local - Authenticate via a key exchanged in a file.
1342: o ssl - Authenticate via a key exchaned through a temporary ssl tunnel.
1343: o insecure - Exchange keys in an insecure manner.
1344:
1345: insecure is only allowed if the configuration parameter loncAllowInsecure
1346: is nonzero.
1347:
1.3 albertel 1348: =item TransactionRequest
1349:
1350: The request being transmitted.
1351:
1352: =item TransactionReply
1353:
1354: The reply being received from the transaction.
1355:
1356: =item InformReadable
1357:
1358: True if we want to be called when socket is readable.
1359:
1360: =item InformWritable
1361:
1362: True if we want to be informed if the socket is writable.
1363:
1364: =item Timeoutable
1365:
1366: True if the current operation is allowed to timeout.
1367:
1368: =item TimeoutValue
1369:
1370: Number of seconds in the timeout.
1371:
1372: =item TimeoutRemaining
1373:
1374: Number of seconds left in the timeout.
1375:
1376: =item CipherKey
1377:
1378: The key that was negotiated with the peer.
1379:
1380: =item Cipher
1381:
1382: The cipher obtained via the key.
1.1 foxr 1383:
1384:
1385: =head2 The following are callback like members:
1.3 albertel 1386:
1387: =item Tick:
1388:
1389: Called in response to a timer tick. Used to managed timeouts etc.
1390:
1391: =item Readable:
1392:
1393: Called when the socket becomes readable.
1394:
1395: =item Writable:
1396:
1397: Called when the socket becomes writable.
1398:
1399: =item TimedOut:
1400:
1401: Called when a timed operation timed out.
1402:
1.1 foxr 1403:
1404: =head2 The following are operational member functions.
1.3 albertel 1405:
1406: =item InitiateTransaction:
1407:
1408: Called to initiate a new transaction
1409:
1410: =item SetStateTransitionCallback:
1411:
1412: Called to establish a function that is called whenever the object goes
1413: through a state transition. This is used by The client to manage the
1414: work flow for the object.
1415:
1416: =item SetTimeoutCallback:
1417:
1418: Set a function to be called when a transaction times out. The
1419: function will be called with the object as its sole parameter.
1420:
1421: =item Encrypt:
1422:
1423: Encrypts a block of text according to the cipher negotiated with the
1424: peer (assumes the text is a command).
1425:
1426: =item Decrypt:
1427:
1428: Decrypts a block of text according to the cipher negotiated with the
1429: peer (assumes the block was a reply.
1.5 foxr 1430:
1431: =item Shutdown:
1432:
1433: Shuts off the socket.
1.1 foxr 1434:
1435: =head2 The following are selector member functions:
1436:
1.3 albertel 1437: =item GetState:
1438:
1439: Returns the current state
1440:
1441: =item GetSocket:
1442:
1443: Gets the socekt open on the connection to lond.
1444:
1445: =item WantReadable:
1446:
1447: true if the current state requires a readable event.
1448:
1449: =item WantWritable:
1450:
1451: true if the current state requires a writable event.
1452:
1453: =item WantTimeout:
1454:
1455: true if the current state requires timeout support.
1456:
1457: =item GetHostIterator:
1458:
1459: Returns an iterator into the host file hash.
1460:
1.1 foxr 1461: =cut
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>