session monitor updates
authorivan <ivan>
Sun, 3 Dec 2000 20:25:20 +0000 (20:25 +0000)
committerivan <ivan>
Sun, 3 Dec 2000 20:25:20 +0000 (20:25 +0000)
16 files changed:
FS/FS/Record.pm
FS/FS/nas.pm
FS/FS/port.pm
FS/FS/session.pm
TODO
fs_sesmon/FS-SessionClient/MANIFEST
fs_sesmon/FS-SessionClient/Makefile.PL
fs_sesmon/FS-SessionClient/SessionClient.pm
fs_sesmon/FS-SessionClient/bin/freeside-login
fs_sesmon/FS-SessionClient/bin/freeside-logoff [deleted file]
fs_sesmon/FS-SessionClient/bin/freeside-logout [new file with mode: 0644]
fs_sesmon/FS-SessionClient/fs_sessiond
fs_sesmon/FS-SessionClient/test.pl
fs_sesmon/fs_session_server
htdocs/browse/nas.cgi
htdocs/view/svc_acct.cgi

index 59472c8..18541d2 100644 (file)
@@ -149,22 +149,32 @@ sub create {
   }
 }
 
-=item qsearch TABLE, HASHREF
+=item qsearch TABLE, HASHREF, SELECT, EXTRA_SQL
 
 Searches the database for all records matching (at least) the key/value pairs
 in HASHREF.  Returns all the records found as `FS::TABLE' objects if that
 module is loaded (i.e. via `use FS::cust_main;'), otherwise returns FS::Record
 objects.
 
+###oops, argh, FS::Record::new only lets us create database fields.
+#Normal behaviour if SELECT is not specified is `*', as in
+#C<SELECT * FROM table WHERE ...>.  However, there is an experimental new
+#feature where you can specify SELECT - remember, the objects returned,
+#although blessed into the appropriate `FS::TABLE' package, will only have the
+#fields you specify.  This might have unwanted results if you then go calling
+#regular FS::TABLE methods
+#on it.
+
 =cut
 
 sub qsearch {
-  my($table, $record) = @_;
+  my($table, $record, $select, $extra_sql ) = @_;
+  $select ||= '*';
   my $dbh = dbh;
 
   my @fields = grep exists($record->{$_}), fields($table);
 
-  my $statement = "SELECT * FROM $table";
+  my $statement = "SELECT $select FROM $table";
   if ( @fields ) {
     $statement .= " WHERE ". join(' AND ', map {
       if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) {
@@ -178,6 +188,7 @@ sub qsearch {
       }
     } @fields );
   }
+  $statement .= " $extra_sql" if defined($extra_sql);
 
   warn $statement if $DEBUG;
   my $sth = $dbh->prepare_cached($statement) or croak $dbh->errstr;
@@ -895,7 +906,7 @@ sub hfields {
 
 =head1 VERSION
 
-$Id: Record.pm,v 1.9 2000-11-07 15:00:37 ivan Exp $
+$Id: Record.pm,v 1.10 2000-12-03 20:25:20 ivan Exp $
 
 =head1 BUGS
 
index 873c9bc..53e0fbc 100644 (file)
@@ -2,8 +2,8 @@ package FS::nas;
 
 use strict;
 use vars qw( @ISA );
-use FS::Record qw();
-#use FS::Record qw( qsearch qsearchs );
+use FS::Record qw(qsearchs); #qsearch);
+use FS::UID qw( dbh ); #to lock the tables for heartbeat; ugh, MySQL-specific
 
 @ISA = qw(FS::Record);
 
@@ -124,18 +124,32 @@ Updates the timestamp for this nas
 =cut
 
 sub heartbeat {
-  warn "warning: heartbeat unimplemented!"
+  my($self, $timestamp) = @_;
+  my $dbh = dbh;
+  my $sth = $dbh->prepare("LOCK TABLES nas WRITE");
+  $sth->execute or die $sth->errstr; #die?
+  my $lock_self = qsearchs('nas', { 'nasnum' => $self->nasnum } )
+    or die "can't find own record for $self nasnum ". $self->nasnum;
+  if ( $timestamp > $lock_self->last ) {
+    my $new_self = new FS::nas ( { $lock_self->hash } );
+    $new_self->last($timestamp);
+    #is there a reason to? #$self->last($timestamp);
+    $new_self->replace($lock_self);
+  };
+  $sth = $dbh->prepare("UNLOCK TABLES");
+  $sth->execute or die $sth->errstr; #die?
 }
 
 =back
 
 =head1 VERSION
 
-$Id: nas.pm,v 1.2 2000-11-07 15:00:37 ivan Exp $
+$Id: nas.pm,v 1.3 2000-12-03 20:25:20 ivan Exp $
 
 =head1 BUGS
 
-The author forgot to customize this manpage.
+The B<heartbeat> method is MySQL-specific.  Yuck.  It's also not quite
+perfectly subclassable, which is much less yuck.
 
 =head1 SEE ALSO
 
index ee4611d..e96e71c 100644 (file)
@@ -118,23 +118,38 @@ sub check {
 
 =item session
 
-Returns the currently open session, or if no session is currently open, the
-most recent session.  See L<FS::session>.
+Returns the currently open session on this port, or if no session is currently
+open, the most recent session.  See L<FS::session>.
 
 =cut
 
-
+sub session {
+  my $self = shift;
+  qsearchs('session', { 'portnum' => $self->portnum }, '*',
+                     'ORDER BY login DESC LIMIT 1' );
+}
 
 =back
 
 =head1 VERSION
 
-$Id: port.pm,v 1.2 2000-12-03 13:44:05 ivan Exp $
+$Id: port.pm,v 1.3 2000-12-03 20:25:20 ivan Exp $
 
 =head1 BUGS
 
 The author forgot to customize this manpage.
 
+The session method won't deal well if you have multiple open sessions on a
+port, for example if your RADIUS server drops B<stop> records.  Suggestions for
+how to deal with this sort of lossage welcome; should we close the session
+when we get a new session on that port?  Tag it as invalid somehow?  Close it
+one second after it was opened?  *sigh*  Maybe FS::session shouldn't let you
+create overlapping sessions, at least folks will find out their logging is
+dropping records.
+
+If you think the above refers multiple user logins you need to read the
+manpages again.
+
 =head1 SEE ALSO
 
 L<FS::Record>, schema.html from the base documentation.
@@ -147,8 +162,8 @@ added hfields
 ivan@sisd.com 97-nov-13
 
 $Log: port.pm,v $
-Revision 1.2  2000-12-03 13:44:05  ivan
-beginnings of web status for session monitor
+Revision 1.3  2000-12-03 20:25:20  ivan
+session monitor updates
 
 Revision 1.1  2000/10/27 20:18:32  ivan
 oops, also necessary for session monitor
index b85a582..027708d 100644 (file)
@@ -94,6 +94,9 @@ sub insert {
   $error = $self->check;
   return $error if $error;
 
+  return "a session on that port is already open!"
+    if qsearchs('session', { 'portnum' => $self->portnum, 'logout' => '' } );
+
   $self->setfield('login', time()) unless $self->getfield('login');
 
   $error = $self->SUPER::insert;
@@ -124,7 +127,7 @@ it is replaced with the current time.
 =cut
 
 sub replace {
-  my $self = shift;
+  my($self, $old) = @_;
   my $error;
 
   local $SIG{HUP} = 'IGNORE';
@@ -139,7 +142,7 @@ sub replace {
 
   $self->setfield('logout', time()) unless $self->getfield('logout');
 
-  $error = $self->SUPER::replace;
+  $error = $self->SUPER::replace($old);
   return $error if $error;
 
   $self->nas_heartbeat($self->getfield('logout'));
@@ -188,15 +191,32 @@ sub nas_heartbeat {
   $nas->heartbeat(shift);
 }
 
+=item svc_acct
+
+Returns the svc_acct record associated with this session (see L<FS::svc_acct>).
+
+=cut
+
+sub svc_acct {
+  my $self = shift;
+  qsearchs('svc_acct', { 'svcnum' => $self->svcnum } );
+}
+
 =back
 
 =head1 VERSION
 
-$Id: session.pm,v 1.2 2000-11-07 15:00:37 ivan Exp $
+$Id: session.pm,v 1.3 2000-12-03 20:25:20 ivan Exp $
 
 =head1 BUGS
 
-The author forgot to customize this manpage.
+Maybe you shouldn't be able to insert a session if there's currently an open
+session on that port.  Or maybe the open session on that port should be flagged
+as problematic?  autoclosed?  *sigh*
+
+Hmm, sessions refer to current svc_acct records... probably need to constrain
+deletions to svc_acct records such that no svc_acct records are deleted which
+have a session (even if long-closed).
 
 =head1 SEE ALSO
 
diff --git a/TODO b/TODO
index c6d7e43..4d0a0ba 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,4 +1,4 @@
-$Id: TODO,v 1.52 2000-10-27 20:15:50 ivan Exp $
+$Id: TODO,v 1.53 2000-12-03 20:25:20 ivan Exp $
 
 If you are interested in helping with any of these, please join the
 *development* mailing list (send a blank message to
@@ -6,6 +6,8 @@ ivan-freeside-devel-subscribe@sisd.com) to avoid duplication of effort.
 
 ---
 
+first package select field in edit/cust_main.cgi isn't sticky on errors, yuck
+
 > 1. A Web Form to the user get his account added automatically . The
 > /etc/raddb/users and /etc/passwd would be updated automatically (these
 > file are on the same machine Freeside is). I guess the the Add
index 6da7b22..9da279a 100644 (file)
@@ -6,6 +6,6 @@ SessionClient.pm
 test.pl
 fs_sessiond
 cgi/logon.cgi
-cgi/logoff.cgi
+cgi/logout.cgi
 bin/freeside-login
-bin/freeside-logoff
+bin/freeside-logout
index 1f59847..137b6b8 100644 (file)
@@ -4,7 +4,7 @@ use ExtUtils::MakeMaker;
 WriteMakefile(
     'NAME'          => 'FS::SessionClient',
     'VERSION_FROM'  => 'SessionClient.pm', # finds $VERSION
-    'EXE_FILES'     => [ qw(fs_sessiond freeside-login freeside-logoff) ],
+    'EXE_FILES'     => [ qw(fs_sessiond bin/freeside-login bin/freeside-logout) ],
     'INSTALLSCRIPT' => '/usr/local/sbin',
     'PERM_RWX'      => '750',
 );
index 97332cb..8a0ff70 100644 (file)
@@ -10,7 +10,7 @@ use IO::Handle;
 $VERSION = '0.01';
 
 @ISA = qw( Exporter );
-@EXPORT_OK = qw( login logoff portnum );
+@EXPORT_OK = qw( login logout portnum );
 
 $fs_sessiond_socket = "/usr/local/freeside/fs_sessiond_socket";
 
@@ -30,7 +30,7 @@ FS::SessionClient - Freeside session client API
 
 =head1 SYNOPSIS
 
-  use FS::SessionClient qw( login portnum logoff );
+  use FS::SessionClient qw( login portnum logout );
 
   $error = login ( {
     'username' => $username,
@@ -43,10 +43,10 @@ FS::SessionClient - Freeside session client API
   $portnum = portnum( { 'nasnum' => $nasnum, 'nasport' => $nasport } )
     or die "unknown nasnum/nasport";
 
-  $error = logoff ( {
+  $error = logout ( {
     'username' => $username,
     'password' => $password,
-    'logoff'   => $timestamp,
+    'logout'   => $timestamp,
     'portnum'  => $portnum,
   } );
 
@@ -73,13 +73,13 @@ Returns a scalar error message, or the empty string for success.
 =item portnum
 
 HASHREF should contain a single key: ip, or the two keys: nasnum and nasport.
-Returns a portnum suitable for the login and logoff subroutines, or false
+Returns a portnum suitable for the login and logout subroutines, or false
 on error.
 
-=item logoff HASHREF
+=item logout HASHREF
 
-HASHREF should have the following keys: usrename, password, logoff and portnum.
-logoff is a UNIX timestamp; if not specified, will default to the current time.
+HASHREF should have the following keys: usrename, password, logout and portnum.
+logout is a UNIX timestamp; if not specified, will default to the current time.
 Starts a new session for the specified user and portnum.  The password is
 optional, but must be correct if specified.
 
@@ -89,9 +89,11 @@ Returns a scalar error message, or the empty string for success.
 
 sub AUTOLOAD {
   my $hashref = shift;
+  my $method = $AUTOLOAD;
+  $method =~ s/^.*:://;
   socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
   connect(SOCK, sockaddr_un($fs_sessiond_socket)) or die "connect: $!";
-  print SOCK "$AUTOLOAD\n";
+  print SOCK "$method\n";
 
   print SOCK join("\n", %{$hashref}, 'END' ), "\n";
   SOCK->flush;
@@ -104,7 +106,7 @@ sub AUTOLOAD {
 
 =head1 VERSION
 
-$Id: SessionClient.pm,v 1.2 2000-11-07 15:00:37 ivan Exp $
+$Id: SessionClient.pm,v 1.3 2000-12-03 20:25:20 ivan Exp $
 
 =head1 BUGS
 
index 6ca4455..a6d4751 100644 (file)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -Tw
 
-#false-laziness hack w freeside-logoff
+#false-laziness hack w freeside-logout
 
 use strict;
 use FS::SessionClient qw( login portnum );
diff --git a/fs_sesmon/FS-SessionClient/bin/freeside-logoff b/fs_sesmon/FS-SessionClient/bin/freeside-logoff
deleted file mode 100644 (file)
index f7b876b..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/perl -Tw
-
-#false-laziness hack w freeside-login
-
-use strict;
-use FS::SessionClient qw( logoff portnum );
-
-my $username = shift;
-
-my $portnum;
-if ( scalar(@ARGV) == 1 ) {
-  my $arg = shift;
-  if ( $arg =~ /^(\d+)$/ ) {
-    $portnum = $1;
-  } elsif ( $arg =~ /^([\d\.]+)$/ ) {
-    $portnum = portnum( { 'ip' => $1 } ) or die "unknown ip!"
-  } else {
-    &usage;
-  }
-} elsif ( scalar(@ARGV) == 2 ) {
-  $portnum = portnum( { 'nasnum' => shift, 'nasport' => shift } )
-    or die "unknown nasnum/nasport";
-} else {
-  &usage;
-}
-
-my $error = login ( {
-  'username' => $username,
-  'portnum'  => $portnum,
-} );
-
-warn $error if $error;
-
-sub usage {
-  die "Usage:\n\n  freeside-logoff username ( portnum | ip | nasnum nasport )";
-}
-
diff --git a/fs_sesmon/FS-SessionClient/bin/freeside-logout b/fs_sesmon/FS-SessionClient/bin/freeside-logout
new file mode 100644 (file)
index 0000000..9b4ecfe
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/perl -Tw
+
+#false-laziness hack w freeside-login
+
+use strict;
+use FS::SessionClient qw( logout portnum );
+
+my $username = shift;
+
+my $portnum;
+if ( scalar(@ARGV) == 1 ) {
+  my $arg = shift;
+  if ( $arg =~ /^(\d+)$/ ) {
+    $portnum = $1;
+  } elsif ( $arg =~ /^([\d\.]+)$/ ) {
+    $portnum = portnum( { 'ip' => $1 } ) or die "unknown ip!"
+  } else {
+    &usage;
+  }
+} elsif ( scalar(@ARGV) == 2 ) {
+  $portnum = portnum( { 'nasnum' => shift, 'nasport' => shift } )
+    or die "unknown nasnum/nasport";
+} else {
+  &usage;
+}
+
+my $error = logout ( {
+  'username' => $username,
+  'portnum'  => $portnum,
+} );
+
+warn $error if $error;
+
+sub usage {
+  die "Usage:\n\n  freeside-logout username ( portnum | ip | nasnum nasport )";
+}
index 74d3aab..d7284b2 100644 (file)
@@ -12,7 +12,7 @@ use vars qw( $Debug );
 
 $Debug = 1;
 
-my $fs_session_socket = "/usr/local/freeside/fs_sessiond_socket";
+my $fs_sessiond_socket = "/usr/local/freeside/fs_sessiond_socket";
 
 $ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
 $ENV{'SHELL'} = '/bin/sh';
@@ -29,10 +29,10 @@ warn "$me starting\n" if $Debug;
 #nothing to read from server
 
 warn "$me creating $fs_sessiond_socket\n" if $Debug;
-my $uaddr = sockaddr_un($fs_signupd_socket);
+my $uaddr = sockaddr_un($fs_sessiond_socket);
 my $proto = getprotobyname('tcp');
 socket(Server,PF_UNIX,SOCK_STREAM,0) or die "socket: $!";
-unlink($fs_signup_socket);
+unlink($fs_sessiond_socket);
 bind(Server, $uaddr) or die "bind: $!";
 listen(Server,SOMAXCONN) or die "listen: $!";
 
@@ -44,8 +44,8 @@ for ( ; $paddr = accept(Client,Server); close Client) {
 
   if ( $command eq 'login' || $command eq 'logout' || $command eq 'portnum' ) {
     warn "$me reading data from local client\n" if $Debug;
-    my @data, $dos;
-    push @data, scalar(<Client>) until $dos++ == 99 || $data[$#data] != "END\n";
+    my( @data, $dos );
+    push @data, scalar(<Client>) until $dos++ == 99 || $data[$#data] eq "END\n";
     if ( $dos == 99 ) { 
       warn "$me WARNING: DoS attempt!" 
     } else {
index d05201b..4b9ae17 100644 (file)
@@ -8,7 +8,8 @@
 
 BEGIN { $| = 1; print "1..1\n"; }
 END {print "not ok 1\n" unless $loaded;}
-use FS::SessionClient;
+#use FS::SessionClient;
+#sigh, "not running as the freeside user"
 $loaded = 1;
 print "ok 1\n";
 
index 46e53d1..0930a3c 100644 (file)
@@ -6,9 +6,9 @@
 use strict;
 use vars qw( $opt $Debug );
 use IO::Handle;
-use Net::SSH qw(sshopen3)
+use Net::SSH qw(sshopen2);
 use FS::UID qw(adminsuidsetup);
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearchs ); #qsearch );
 #use FS::cust_main_county;
 #use FS::cust_main;
 use FS::session;
@@ -31,25 +31,28 @@ while (1) {
   my($reader, $writer) = (new IO::Handle, new IO::Handle);
   $writer->autoflush(1);
   warn "$me Connecting to $machine\n" if $Debug;
-  sshopen2($machine,$reader,$writer,$fs_signupd);
+  sshopen2($machine,$reader,$writer,$fs_sessiond);
 
   warn "$me Entering main loop\n" if $Debug;
   while (1) {
     warn "$me Reading (waiting for) data\n" if $Debug;
-    my $command = scalar(<$reader));
+    my $command = scalar(<$reader>);
+    chomp $command;
     #DoS protection here too, to protect against a compromised client?  *sigh*
-    while ( ( my $key = scalar(<$reader>) ) != "END\n" ) {
+    my %hash;
+    while ( ( my $key = scalar(<$reader>) ) ne "END\n" ) {
       chomp $key;
       chomp( $hash{$key} = scalar(<$reader>) );
     }
 
     if ( $command eq 'login' ) {
-      $error = &login(\%hash);
+      my $error = &login(\%hash);
       print $writer "$error\n";
-    } elsif ( $command eq 'logoff' ) {
-      $error = &logoff(\%hash);
+    } elsif ( $command eq 'logout' ) {
+      my $error = &logout(\%hash);
       print $writer "$error\n";
     } elsif ( $command eq 'portnum' ) {
+      my $port;
       if ( exists $hash{'ip'} ) {
         $hash{'ip'} =~ /^([\d\.]+)$/ or $1='nomatch';
         $port = qsearchs('port', { 'ip' => $1 } );
@@ -60,13 +63,13 @@ while (1) {
       }
       print $writer ( $port ? $port->portnum : '' ), "\n";
     } else {
-      warn "$me WARNING: unrecognized command";
+      warn "$me WARNING: unrecognized command: $command";
     }
   }
   #won't ever reach without code above to throw out of loop, but...
   close $writer;
   close $reader;
-  warn "connection to $machine lost!\n"
+  warn "connection to $machine lost!\n";
   sleep 5;
   warn "reconnecting...\n";
 }
@@ -75,10 +78,10 @@ sub login {
   my $href = shift;
   $href->{'username'} =~ /^([a-z0-9_\-\.]+)$/ or return "Illegal username";
   my $username = $1;
-  $svc_acct = qsearchs('svc_acct', { 'username' => $username } )
+  my $svc_acct = qsearchs('svc_acct', { 'username' => $username } )
     or return "Unknown user";
   return "Incorrect password"
-    if defined($href->{'password'})
+    if exists($href->{'password'})
        && $href->{'password'} ne $svc_acct->_password;
   my $session = new FS::session {
     'portnum' => $href->{'portnum'},
@@ -92,18 +95,19 @@ sub logout {
   my $href = shift;
   $href->{'username'} =~ /^([a-z0-9_\-\.]+)$/ or return "Illegal username";
   my $username = $1;
-  $svc_acct = qsearchs('svc_acct', { 'username' => $username } )
+  my $svc_acct = qsearchs('svc_acct', { 'username' => $username } )
     or return "Unknown user";
   return "Incorrect password"
-    if defined($href->{'password'})
+    if exists($href->{'password'})
        && $href->{'password'} ne $svc_acct->_password;
-  my $session = qsearchs FS::session {
+  my $session = qsearchs( 'session', {
     'portnum' => $href->{'portnum'},
     'svcnum'  => $svc_acct->svcnum,
-    'logoff'  => '',
-  };
-  return "No currently open sessios found for that user/port!" unless $session;
-  my $nsession = new FS::session ( { $old->hash } );
+    'logout'  => '',
+  } );
+  return "No currently open sessions found for that user/port!" unless $session;
+  my $nsession = new FS::session ( { $session->hash } );
+  warn "$nsession replacing $session";
   $nsession->replace($session);
 }
 
index 2aafbc1..a65235b 100755 (executable)
@@ -33,10 +33,29 @@ foreach my $nas ( sort { $a->nasnum <=> $b->nasnum } qsearch( 'nas', {} ) ) {
   ;
   foreach my $port ( sort {
     $a->nasport <=> $b->nasport || $a->portnum <=> $b->portnum
-  } qsearch( 'port' ) ) {
+  } qsearch( 'port', { 'nasnum' => $nas->nasnum } ) ) {
+    my $session = $port->session;
+    my($user, $since, $pretty_since, $duration);
+    if ( ! $session ) {
+      $user = "(empty)";
+      $since = 0;
+      $pretty_since = "(never)";
+      $duration = '';
+    } elsif ( $session->logout ) {
+      $user = "(empty)";
+      $since = $session->logout;
+    } else {
+      my $svc_acct = $session->svc_acct;
+      $user = "<A HREF=\"$p/view/svc_acct.cgi?". $svc_acct->svcnum. "\">".
+              $svc_acct->username. "</A>";
+      $since = $session->login;
+    }
+    $pretty_since = time2str("%c", $since) if $since;
+    $duration = pretty_interval( $now - $since ). " ago"
+      unless defined($duration);
     print "<TR><TD>". $port->nasport. "</TD><TD>". $port->portnum. "</TD><TD>".
-          $port->ip. "</TD><TD>". 'user'. "</TD><TD>". 'since'. "</TD><TD>". 
-          'duration'. "</TD></TR>"
+          $port->ip. "</TD><TD>$user</TD><TD>$pretty_since".
+          "</TD><TD>$duration</TD></TR>"
     ;
   }
   print "</TABLE><BR>";
index e203c7e..22c3d76 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -Tw
 #
-# $Id: svc_acct.cgi,v 1.10 1999-04-14 11:27:06 ivan Exp $
+# $Id: svc_acct.cgi,v 1.11 2000-12-03 20:25:20 ivan Exp $
 #
 # Usage: svc_acct.cgi svcnum
 #        http://server.name/path/svc_acct.cgi?svcnum
 # displays arbitrary radius attributes ivan@sisd.com 98-aug-16
 #
 # $Log: svc_acct.cgi,v $
-# Revision 1.10  1999-04-14 11:27:06  ivan
+# Revision 1.11  2000-12-03 20:25:20  ivan
+# session monitor updates
+#
+# Revision 1.10  1999/04/14 11:27:06  ivan
 # showpasswords config option to show passwords
 #
 # Revision 1.9  1999/04/08 12:00:19  ivan
@@ -138,7 +141,7 @@ $password = '';
 
 $svc_acct_pop = qsearchs('svc_acct_pop',{'popnum'=>$svc_acct->popnum});
 print "<BR>POP: <B>", $svc_acct_pop->city, ", ", $svc_acct_pop->state,
-      " (", $svc_acct_pop->ac, ")/", $svc_acct_pop->exch, "<\B>"
+      " (", $svc_acct_pop->ac, ")/", $svc_acct_pop->exch, "</B>"
   if $svc_acct_pop;
 
 if ($svc_acct->uid ne '') {