#!/usr/bin/perl

# This utility translates from aspppd configuration to Solaris PPP 4.0
# (or ANU ppp-2.4.0; aka pppd).  It can also revert to previous aspppd
# configuration (discarding the pppd configuration), but does not
# translate new configuration files into old.
#
# This script provides only a suggested translation for your existing
# aspppd configuration.  You will need to evaluate for yourself
# whether the translation is appropriate for your operating
# environment.

# Copyright (c) 2000 by Sun Microsystems, Inc.
# All rights reserved.
#
#ident	"%Z%%M%	%I%	%E% SMI"

# Steps in translation:
#	- parse /etc/asppp.cf
#	- check for aspppls in /etc/passwd (or NIS or NIS+)
#	- read in current /etc/ppp/options configuration file
#	- read list of configured serial ports from pmadm
#	- read in UUCP configuration files
#	- create translated configuration
#	- write files back out

# Known issues:
#	- translation with point-to-multipoint is incomplete

use Getopt::Std;
use Fcntl;
use POSIX qw(tmpnam ENOSYS);
use Sys::Hostname;

# Secure the path if we're running under RBAC.
$ENV{PATH} = ( "/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/ucb" )
    if $< != $>;

# General path names that can be configured.
local($rootetc) =	"/etc/";
local($passwd) =	$rootetc . "passwd";
local($passwdlck) =	$rootetc . ".pwd.lock";
local($asfile) =	$rootetc . "asppp.cf";
local($astemp) =	$rootetc . "asppp.temp.cf";
local($asmoved) =	$rootetc . "asppp.saved.cf";
local($uucpdir) =	$rootetc . "uucp/";
local($Devices) =	$uucpdir . "Devices";
local($Devconfig) =	$uucpdir . "Devconfig";
local($Dialers) =	$uucpdir . "Dialers";
local($Dialcodes) =	$uucpdir . "Dialcodes";
local($Limits) =	$uucpdir . "Limits";
local($Sysfiles) =	$uucpdir . "Sysfiles";
local($Systems) =	$uucpdir . "Systems";
local($pppdir) =	$rootetc . "ppp/";
local($options) =	$pppdir . "options";
local($ttyprefix) =	$pppdir . "options.";
local($peersdir) =	$pppdir . "peers/";
local($initd) =		$rootetc . "init.d/";
local($asctl) =		$initd . "asppp";
local($pppdctl) =	$initd . "pppd";
local($sedpasswd) =	"/tmp/sed-passwd";

# Fake asppp keyword used to keep track of dial-in paths.
local($isdialin) = "-is-dial-in";

# Limits and Sysfiles are keyed on "service=".
# Devconfig is keyed on "service=" and "device=".
# Dialcodes, Dialers, Systems, and Devices are single keyword files.
# Devices alone may have multiple entries for a given key.

# Internal data structures
local(@sysfiles,@limits,@devconfig);
local(@sysdefault) = ( "systems=" . $Systems, "dialers=" . $Dialers,
		       "devices=" . $Devices );
local(@limitdefault) = ( "max=-1" );
local(@devdefault) = ( "pop=", "push=", "connecttime=-1", "expecttime=-1",
		       "msgtime=-1" );

# List of keywords for which ifconfig takes an additional parameter.
local($ifconfigtakes) = (
	addif => 1,
	removeif => 1,
	auth_algs => 1,
	encr_algs => 1,
	encr_auth_algs => 1,
	broadcast => 1,
	destination => 1,
	index => 1,
	metric => 1,
	modinsert => 1,
	modremove => 1,
	mtu => 1,
	netmask => 1,
	set => 1,
	subnet => 1,
	tdst => 1,
	tsrc => 1,
	wait => 1,

# These are keywords, but do not take an additional parameter.
	ether => 0,
	inet => 0,
	inet6 => 0,
	arp => 0,
	-arp => 0,
	auto-revarp => 0,
	modlist => 0,
	plumb => 0,
	unplumb => 0,
	private => 0,
	-private => 0,
	nud => 0,
	-nud => 0,
	trailers => 0,
	-trailers => 0,
	up => 0,
	down => 0,
	xmit => 0,
	-xmit => 0,
	auto-dhcp => 0,
	dhcp => 0,
	primary => 0,
	drop => 0,
	extend => 0,
	inform => 0,
	ping => 0,
	release => 0,
	start => 0,
	status => 0
);

# print number of something in English.
sub nof
{
    local($num, $item, @rest) = @_;
    print "No ", $item, "s", @rest if $num == 0;
    print "1 ", $item, @rest if $num == 1;
    print $num, " ", $item, "s", @rest if $num > 1;
}

# ask a yes or no question.
sub yesno
{
    local ($query, $default) = @_;
    local ($ans, $defans);

    return $default unless (-t STDIN) && (-t STDOUT) && !$opt_n;
    $defans = $default ? "Yn" : "yN";
    while (1) {
	print $query, " [", $defans, "]? ";
	chomp($ans = <STDIN>);
	return $default unless $ans;
	return 1 if $ans =~ /^[Yy1Tt]/;
	return 0 if $ans =~ /^[Nn0Ff]/;
	print "Please enter 'y' or 'n'.\n";
    }
}

# Put quotes around a string, if necessary.
# The tests here aren't perfect -- they think that \\\' isn't an
# escaped quote -- but they're good enough.
sub requote
{
    local($_) = @_;
    if (/^$/) {
	"\"\"";
    } elsif (/^'/ || /[^\\]'/ || /\\\\'/) {
# Has unescaped quotes; must use " or redo quoting.
	if (/^"/ || /[^\\]"/ || /\\\\"/) {
# Both kinds of quotes; redo the quoting.
	    s/^"/\\"/;
	    s/([^\\]|\\\\)"/$1\\"/g;
	}
	"\"" . $_ . "\"";
    } elsif (/^"/ || /[^\\]"/ || /\\\\"/) {
	"'" . $_ . "'";
    } elsif (/\s/) {
	"\"" . $_ . "\"";
    } else {
	$_;
    }
}

# Get a single line from a UUCP configuration file and return as a
# reference to an array of words.  Removes comments and escapes.
# (This is a modified version of the standard Perl shellwords function
# that understands C escape sequences and continuation lines.)
# Optionally returns lead-in, source text, and trailing component
# for editing.
sub uucpline
{
    local($input, $file, $triplet) = @_;
    local(@words,$snippet,$field,$havefield,$cont,@triparray,$maytrail);

    $cont = "";
    $maytrail = "";
    while (<$input>) {
	# remove leading whitespace
	if (s/^(\s+)//) {
	    $maytrail .= $1;
	}
	if ($cont eq "") {
	    if (s/^(\#(.|\n)*)$//) {
		$triparray[0] .= $maytrail . $1;
		$maytrail = "";
		next;
	    }
	    if (s/^(\\?\n?)$//) {
		$triparray[0] .= $maytrail . $1;
		$maytrail = "";
		next;
	    }
	    $triparray[0] .= $maytrail;
	    $maytrail = "";
	}
	$snippet = $_;
	if (s/^(([^\#\\]|\\.)*)\\\n$//) {
	    $maytrail .= $snippet;
	    $cont .= $1;
	    next;
	}
	if (/^(([^\\\#]|\\[^\#])*)(\#?(.|\n)*)$/) {
	    $_ = $maytrail . $1;
	    $maytrail = $3;
	    if (s/((\s|\n)*)$//) {
		$maytrail = $1 . $maytrail;
	    }
	    $triparray[1] = $_;
	}
	$_ = $cont . $snippet;
	$cont = "";
	s/\n$//;
	while ($_ ne '') {
	    for (;;) {
		if (s/^#.*//) {
		    last;
		} elsif (s/^"(([^"\\]|\\.)*)"//) {
		    $snippet = $1;
		} elsif (s/^"//) {
		    warn "Unmatched double quote in $file: \"$_\n";
		} elsif (s/^'(([^'\\]|\\.)*)'//) {
		    $snippet = $1;
		} elsif (s/^'//) {
		    warn "Unmatched single quote in $file: '$_\n";
		} elsif (s/^\\s//) {
# \s works in chat, but not in the pppd option files
		    $snippet = " ";
		} elsif (s/^(\\.)//) {
		    $snippet = $1;
		} elsif (s/^([^\s\\'"#]+)//) {
		    $snippet = $1;
		} else {
		    s/^\s+//;
		    last;
		}
		$havefield = 1;
		$field .= $snippet;
	    }
	    push(@words, $field) if $havefield;
	    $havefield = 0;
	    $field = '';
	}
	last;
    }
    $triparray[2] .= $maytrail;
    @$triplet = @triparray;
    warn "Bad continuation line in $file: $cont\n" if $cont ne '';
    \@words;
}

# Given a logical UUCP file name, return a list of all of the files
# that should be read.
sub uucpfiles
{
    local ($file) = @_;
    local (@flist, $value) = ();

    for $value (@sysfiles, @sysdefault) {
	if ($value =~ /^$file=/i) {
	    $value =~ s/^$file=//i;
	    for $file (split /:/, $value) {
		$file = $uucpdir . $file if $file !~ /^\//;
		push @flist, $file;
	    }
	    last;
	}
    }
    @flist;
}

# Given a file name and some key words, parse the contents of the file
# and return a reference to a hash constructed from this.  All keys
# except the last must match exactly.  The last is used to order the
# hash.
sub uucpkeyfile
{
    local($file,@keylist) = @_;
    local($lastkey,$keyval,$words,$i,$flag,%byservice);

    open(SVCFILE, '<' . $file) || return undef;
    $lastkey = pop @keylist;
    while (@{$words = uucpline(SVCFILE, $file)}) {
	$flag = 1;
	foreach $keyval (@keylist) {
	    $flag = 0;
	    $i = 0;
	    while ($i < @$words) {
		if ($$words[$i] eq $keyval) {
		    splice @$words, $i, 1;
		    $flag = 1;
		    last;
		}
		$i++;
	    }
	    last unless $flag;
	}
	next unless $flag;
	foreach $i (0 .. @{$words}) {
	    if (@{$words}[$i] =~ /^$lastkey(.*)/i) {
		splice @{$words}, $i, 1;
		$byservice{$1} = $words;
		last;
	    }
	}
    }
    close SVCFILE;
    \%byservice;
}

# This reads a UUCP file that is keyed on the first token on each
# line.  Duplicates are not permitted; the first encountered is used
# and the rest are silently discarded.  A hash indexed on the first
# token and containing an array of tokens in each bucket is returned.
# Used for Dialcodes, Dialers, and Systems.
sub uucpposfiles
{
    local(@files) = @_;
    local($keyval,$words,%keyeddata);

    foreach $file (@files) {
	if (!open(POSFILE, '<' . $file)) {
	    warn "$file: $!\n";
	    next;
	}
	while (@{$words = uucpline(POSFILE, $file)}) {
	    $keyval = shift @{$words};
	    next if $keyeddata{$keyval};
	    $keyeddata{$keyval} = $words;
	}
	close POSFILE;
    }
    \%keyeddata;
}

# This reads a UUCP file that is keyed on the first token on each line
# and may have duplicate entries.  Each entry of the hash contains an
# array.  Each entry of that array points to an array of tokens
# representing one parsed line.  Used for the Devices file.
sub uucpdevices
{
    local(@files) = @_;
    local($keyval,$words,%keyeddata);

    foreach $file (@files) {
	if (!open(POSFILE, '<' . $file)) {
	    warn "$file: $!\n";
	    next;
	}
	while (@{$words = uucpline(POSFILE, $file)}) {
	    $keyval = shift @{$words};
	    push @{$keyeddata{$keyval}}, $words;
	}
	close POSFILE;
    }
    \%keyeddata;
}

# For a path defined in asppp.cf, copy over defaults, validate the
# required options, and save in the hash to be returned.
sub savepath
{
    local($paths, $options, $defref) = @_;
    local($peer,$intf);

    return if $options == $defref;
    foreach $key (keys %$defref) {
	$$options{$key} = $$defref{$key} unless defined($$options{$key});
    }
    $peer = $$options{"peer_system_name"};
    warn("Discarded path with no peer system name.\n"), return
	unless defined($peer);
    $intf = $$options{"interface"};
    warn("Missing interface on path to peer \"$peer\".\n"), return
	unless defined($intf);
    warn("Bad interface $intf on path to peer \"$peer\".\n"), return
	unless $intf =~ /^ipd([0-9]+|ptp[0-9]+|ptp\*)$/;
    warn("Missing peer IP address for point-to-multipoint path to \"",
	$peer, "\".\n"), return
	if $intf =~ /^ipd[0-9]+$/ && !defined($$options{"peer_ip_address"});
    warn "Multiple definitions of path to peer \"$peer\".\n",
	if defined($paths{$peer});
    warn "Odd version number ", $$options{"version"},
	" encountered in path to peer \"", $peer, "\" (ignored).\n"
	if defined($$options{"version"}) && $$options{"version"} != 1;
    $paths{$peer} = $options;
}

# Parse through asppp.cf. Unlike the UUCP files, lines don't matter
# here.  The parsing is modal, with "path" introducing a PPP session
# description and "defaults" reverting back to global definitions.
sub readaspppcf
{
    local($aspppcf) = @_;
    local(%aspppdkey) = (
	chap_name		=> 1,
	chap_peer_secret	=> 1,
	chap_peer_name		=> 1,
	chap_secret		=> 1,
	debug_level		=> 1,
	default_route		=> 0,
	ifconfig		=> -1,
	inactivity_timeout	=> 1,
	interface		=> 1,
# sic; aspppd is seriously confused!  ACCM isn't in IPCP.
	ipcp_async_map		=> 1,
	ipcp_compression	=> 1,
	lcp_compression		=> 1,
	lcp_mru			=> 1,
	negotiate_address	=> 1,
	pap_id			=> 1,
	pap_password		=> 1,
	pap_peer_id		=> 1,
	pap_peer_password	=> 1,
	peer_ip_address		=> 1,
	peer_system_name	=> 1,
	require_authentication	=> 2,
	version			=> 1,
	will_do_authentication	=> 2
    );
    local($words,$word,$prevword,$i,$errors,%defaults,%ifconfig,%paths);
    local($options);

    open ASPPPD, "<" . $aspppcf || die "$aspppcf: $!\n";
    print "Reading configuration from $aspppcf\n" if $opt_v;
    $defaults{inactivity_timeout} = 120;
    $defaults{ipcp_compression} = "vj";
    $defaults{lcp_compression} = "on";
    $options = \%defaults;
    while (@{$words = uucpline(ASPPPD, $aspppcf)}) {
	if ($$words[0] =~ /^ifconfig$/i) {
	    warn "$prevword with missing argument ignored.\n"
		if defined($prevword);
	    undef $prevword;
	    shift @$words;	# discard 'ifconfig' keyword
	    $word = shift @$words;
	    warn("Bad interface on ifconfig $word.\n"), next
		unless $word =~ /^ipd([0-9]+|ptp[0-9]+)$/;
	    $ifconfig{$word} = \@$words;
	    next;
	}
	unshift @{$words}, $prevword if defined($prevword);
	undef $prevword;
	while ($word = lc(shift @{$words})) {
	    $_ = $word;
	    if (/^defaults$/i) {
		savepath(\%paths, $options, \%defaults);
		$options = \%defaults;
		next ;
	    }
	    if (/^path$/i) {
		local(%pathopts);
		savepath(\%paths, $options, \%defaults);
		$options = \%pathopts;
		next;
	    }
	    if (!defined($i = $aspppdkey{$word})) {
		die "Too many errors in $aspppcf; aborting.\n"
		    if ++$errors > 5;
		warn "Ignoring unknown keyword $word in $aspppcf\n";
		next;
	    }
	    warn("$_ unexpected; remainder of line ignored.\n"),
		last if $i == -1;
	    warn("Duplicate $_ in path ignored.\n"), next
		if $options != \%defaults && defined($$options{$_});
	    $$options{$_} = 1 if $i == 0;
	    next if $i == 0;
	    $prevword = $_, last unless defined($word = shift @{$words});
	    $$options{$_} = $word if $i == 1;
	    if ($i == 2) {
		undef $$options{$_}, next if $word =~ /^off$/;
		$$options{$_} = $word;
		if ($word = shift @{$words}) {
		    if ($word =~ /^(p|ch)ap$/) {
			$$options{$_} .= " " . $word;
		    } else {
			unshift @{$words}, $word;
		    }
		}
	    }
	}
    }
    warn "Odd trailing keyword \"$prevword\" ignored in $aspppcf\n"
	if $prevword;
    savepath(\%paths, $options, \%defaults);
    die "No paths defined for aspppd.\n" if 0+(keys %paths) == 0;
    die "No interfaces defined for aspppd.\n" if 0+(keys %ifconfig) == 0;
    if ($opt_v) {
	nof 0+(keys %paths), "path", " and ";
	nof 0+(keys %ifconfig), "interface", " defined for aspppd.\n";
    }
    close ASPPPD;
    ( \%ifconfig, \%paths );
}

# Read /etc/passwd (or NIS or NIS+) and return hash of users for whom
# the default shell is aspppls.  Each hash entry contains the user's
# home directory path.
sub readpasswd
{
    local(%users,@pwe);

    setpwent();
    while (@pwe = getpwent()) {
	$users{$pwe[0]} = $pwe[7] if $pwe[8] =~ /\/aspppls$/;
    }
    endpwent();
    nof 0+(keys %users), "aspppd dial in user", " found.\n"
	if $opt_v;
    \%users;
}

# Parse through pmadm output to find enabled serial ports.
# Field 9 has 'I' (modem dial-out only or initialize only), 'b'
# (bidirectional) or is blank (modem dial-in only or terminal-hardwired).
# For that latter case, field 18 (software-carrier) has 'y'.
# Field 3 has 'x' if disabled.
sub getserialports
{
    local(%dialin, %dialout);

    open PMADM, "pmadm -L|" || (warn "pmadm: $!\n", return undef);
    while (<PMADM>) {
	split /:/;
	if ($_[3] !~ /x/) {
	    $dialin{$_[2]} = $_[8] if $_[9] ne "I";
	    $dialout{$_[2]} = $_[8] if $_[9] ne "";
	}
    }
    close PMADM;
    ( \%dialin, \%dialout );
}

# Convert an ifconfig statement into a local and remote address pair.
sub ifconf_addr
{
    local($ifconf) = @_;
    local($arg, $narg, $lcladdr, $remaddr);

    shift @$ifconf;	# lose the interface name
    while (@$ifconf) {
	local($arg, $narg);
	$arg = shift @$ifconf;
	$narg = shift @$ifconf if $ifconfigtakes{$arg};
	if (exists($ifconfigtakes{$arg})) {
	    if ($arg eq "set") {
		$lcladdr = $narg;
	    } elsif ($arg eq "destination") {
		$remaddr = $narg;
	    }
	} elsif (!defined($lcladdr)) {
	    $lcladdr = $arg;
	} elsif (!defined($remaddr)) {
	    $remaddr = $arg;
	}
    }
    ( $lcladdr, $remaddr );
}

# Convert a hash of aspppd options into an array of pppd options.  The
# third argument ($chatpath) is undef for dial-in or a path to the
# chat script file otherwise.
sub convert_options
{
    local ($pppdargs, $opts, $chatpath, $user) = @_;

# Do the pppd option conversions.
    push(@$pppdargs, "defaultroute") if $$opts{default_route};
    push(@$pppdargs, "idle " . $$opts{inactivity_timeout})
	if $$opts{inactivity_timeout} != 0;
    push(@$pppdargs, "asyncmap " . $$opts{ipcp_async_map})
	if $$opts{ipcp_async_map};
    push(@$pppdargs, "novj") if !$$opts{ipcp_compression};
    local($peer);
    if ($$opts{require_authentication}) {
	local (@authopts);
	if ($$opts{require_authentication} =~ /chap/) {
	    push(@authopts, "require-chap");
	    $peer = $$opts{chap_peer_name};
	} else {
	    push(@authopts, "require-pap");
	    $peer = $$opts{pap_peer_id};
	}
	push(@authopts, "remotename " . requote($peer)) if $peer;
	push(@authopts, "auth");
	if ($chatpath) {
	    push(@$pppdargs, @authopts);
	} elsif ($dialin_auth == 3) {
# mixed authentication; must use wrapper script.
	    local($sfile) = $pppdir . "dial-in." . $user;
	    $scriptfiles{$sfile}  = "#!/bin/sh\n";
	    $scriptfiles{$sfile} .= "exec /usr/bin/pppd @authopts\n";
	    $dialinshell{$user} = $sfile;
	}
    } elsif ($dialin_auth < 2) {
	push(@$pppdargs, "noauth");
    }
    push(@$pppdargs, "noaccomp nopcomp") if !$$opts{lcp_compression};
    push(@$pppdargs, "mru " . $$opts{lcp_mru}) if $$opts{lcp_mru};
    push(@$pppdargs, "noipdefault ipcp-accept-local")
	if $$opts{negotiate_address};
    local($myname,$csecret,$psecret,$passopt);
    if ($$opts{will_do_authentication} =~ /chap/i) {
	$myname = $$opts{chap_name};
	$csecret = $$opts{chap_secret};
    }
    $myname = "" if !$myname;
    if ($$opts{will_do_authentication} =~ /pap/i) {
	if ($myname ne "" && $$opts{pap_id} ne "" &&
	    $myname ne $$opts{pap_id}) {
	    warn "pppd cannot have separate local names for PAP and CHAP; using CHAP name:\n";
	    warn "\t\"$myname\"\n";
	} else {
	    $myname = $$opts{pap_id} if $$opts{pap_id} ne "";
	}
	$psecret = $$opts{pap_password};
    }
    if ($$opts{will_do_authentication}) {
	if ($chatpath &&
	    ($$opts{will_do_authentication} !~ /chap/i ||
	     $$opts{will_do_authentication} !~ /pap/i ||
	     $csecret eq $psecret)) {
	    push(@$pppdargs,
		"password " . requote($csecret ? $csecret : $psecret));
	    $passopt = 1;
	}
	push @$pppdargs, "user " . requote($myname);
	push(@$pppdargs, "remotename " . requote($peer))
	  if $peer && !$$opts{require_authentication};
    } else {
	$myname = "NeverAuthenticate";
    }

    push(@$pppdargs, "debug") if $$opts{debug_level} >= 8;
    push(@$pppdargs, "kdebug 3") if $$opts{debug_level} >= 9;
    local($lcladdr, $remaddr) = ifconf_addr($$ifconfig{$$opts{interface}});
    if ($chatpath) {
	if ($$opts{debug_level} >= 4) {
	    push(@pppdargs, "connect \"/usr/bin/chat -vf $chatpath\"");
	} else {
	    push(@pppdargs, "connect \"/usr/bin/chat -f $chatpath\"");
	}
	push(@$pppdargs, "user " . requote($myname));
	local($str) = $remaddr;
	$str = $$opts{peer_ip_address} if $$opts{peer_ip_address};
	push(@$pppdargs, $lcladdr . ":" . $str)
	    if !$opt_s && ($lcladdr || $str);
    } else {
	push(@$pppdargs, "user " . requote($myname))
	    if $myname eq "NeverAuthenticate";
    }
    if ($$opts{interface} && $opt_s) {
	if ($$opts{interface} =~ /^ipd[0-9]+$/) {
	    push(@$pppdargs, $lcladdr . ":") if $lcladdr;
	} elsif ($$opts{interface} =~ /^ipd(|ptp)([0-9]+)$/) {
	    push(@pppdargs, "unit " . $2);
	} else {
	    push(@pppdargs, "plumbed");
	}
    }

# Convert the secrets
    if ($chatpath) {
	$addsecret = "";
    } elsif ($$opts{peer_ip_address}) {
	$addsecret = " " . $$opts{peer_ip_address};
    } else {
	$addsecret = " *";
    }
    if ($$opts{require_authentication}) {
	local($lclname, $secret, $authf);
	$lclname = $$opts{will_do_authentication} ? $myname : hostname();
	if ($$opts{require_authentication} =~ /chap/) {
	    $secret = $$opts{chap_peer_secret};
	    $authf = \%chapsecrets;
	} else {
	    $secret = $$opts{pap_peer_password};
	    $authf = \%papsecrets;
	}
	${$$authf{$peer}}{$lclname} = requote($secret) . $addsecret;
    }
    if ($$opts{will_do_authentication} && !$passopt) {
	${$chapsecrets{$myname}}{$peer} = requote($csecret) if $csecret;
	${$papsecrets{$myname}}{$peer} = requote($psecret) if $psecret;
    }
}

# Translate options for a dial-in user.
sub translatedialin
{
    local($peer) = @_;

    $optname = $$dialinusers{$peer};
    $optname .= "/" if $optname !~ /\/$/;
    $optname .= ".ppprc";
    if (exists($optfiles{$optname})) {
	warn "Home directories of $peer and $optuser{$optname} are the same ($optfiles{$optname}\n";
	warn "Ignoring configuration for $peer.\n";
	return;
    }
    $optuser{$optname} = $peer;
    $dialinshell{$peer} = "/usr/bin/pppd";
    local (@pppdargs);
    convert_options(\@pppdargs, $$paths{$peer}, undef, $peer);
    push @pppdargs, "nologfd";
    $optfiles{$optname} = \@pppdargs;
}

# Translate ifconfig entries in asppp.cf into either an ifconfig
# script for strict translation or a set of per-port IP addresses
# for normal translation.  Also translate ipdX interfaces into
# Ethernet aliases to make routing daemon happy.
sub translateifconfig
{
    local (@ifconfiglist,$cstr,$etherif,$intf,$ifconf,$addstr,$port);
    
    open IFLIST, "/usr/sbin/ifconfig -au4|" || die "cannot run ifconfig: $!\n";
    while (<IFLIST>) {
	$etherif = $1 if !$etherif && /^([^:]*).*UP.*BROADCAST/;
    }
    close IFLIST;
    $etherif = $1 if !$etherif && glob("/etc/hostname.*") =~ /[^\.]*.(.*)/;
    $etherif = $1 if !$etherif && glob("/etc/dhcp.*") =~ /[^\.]*.(.*)/;
    $etherif = "hme0" if !$etherif;
    
    $cstr = "";
    foreach $intf (keys %$ifconfig) {
	$ifconf = $$ifconfig{$intf};
	if ($intf =~ /ipd[0-9]+/) {
	    shift @$ifconf;
	    $cstr .= "ifconfig $etherif addif @$ifconf\n";
	}
    }

    $ndialin = 0+(keys %$dialin);
    $addstr = "";
    foreach $intf (keys %downif) {
	$ifconf = $downif{$intf};
	if ($intf =~ /ipdptp([0-9]+)/ && --$ndialin >= 0) {
	    push @ifconfiglist, $ifconf;
	    $addstr .= "ifconfig sppp" . $1 . " @$ifconf\n";
	}
    }
    if ($ndialin > 0) {
	if (@ifconfiglist) {
	    print "Fewer ifconfigs (", $#ifconfiglist+1,
	    ") than dial-in lines (", $ndialin+$#ifconfiglist+1, ")\n";
	} else {
	    print "No ifconfigs for ";
	    nof $ndialin, "dial-in port", ".\n";
	}
    } elsif ($ndialin < 0) {
	if (@ifconfiglist) {
	    print "Ignoring ", -$ndialin, " of ";
	    nof $#ifconfiglist-$ndialin+1, "ifconfig",
	    " (too few dial-in ports)\n";
	} else {
	    print "Ignoring all ";
	    nof -$ndialin, "ifconfig line", " (no dial-in ports)\n";
	}
    }

    if ($opt_s) {
	# Strict translation uses pre-plumbed interfaces.
	$cstr .= $addstr;
    } else {
	foreach $port (values %$dialin) {
	    last if !@ifconfiglist;
	    $port =~ s+/dev/++;
	    $port =~ s+/+.+g;
	    $optfile = $ttyprefix . $port;
	    $ifconf = pop @ifconfiglist;
	    local ($lcladdr, $remaddr) = ifconf_addr($ifconf);
	    next if !defined($lcladdr) || !defined($remaddr);
	    local (@pppdargs) = $lcladdr . ":" . $remaddr;
	    $optfiles{$optfile} = \@pppdargs;
	}
    }
    $scriptfiles{$pppdir . "ifconfig"} = $cstr if $cstr;
}

# Attempt to modify global passwd file using sed script stored in
# the $sedpasswd temporary file.
sub rewrite_passwd
{
    print "Updating local passwd file (if any).\n" if $opt_v;
    if (!sysopen(PWDLCK, $passwdlck, O_WRONLY|O_CREAT, 0600)) {
	warn "Unable to lock password file: $!\n";
    } else {
	$lockstr = pack "ssLLiiLLLL", F_WRLCK, 0, 0, 0, 0, 0, 0, 0, 0, 0;
	eval {
	    local $SIG{ARLM} = sub {
		die "alarm while locking password file\n"
	    };
	    alarm 15;
	    fcntl PWDLCK, F_SETLKW, $lockstr ||
	      die "cannot lock password file: $!\n";
	    alarm 0;
	};
	if ($@) {
	    warn $@;
	} else {
	    warn "Password update failed.\n"
	      if (system("sed -f $sedpasswd < $passwd > ${passwd}.new") ||
		  !(rename "${passwd}.new", $passwd));
	}
	$lockstr = pack "ssLLiiLLLL", F_UNLCK, 0, 0, 0, 0, 0, 0, 0, 0, 0;
	fcntl PWDLCK, F_SETLK, $lockstr;
	close PWDLCK;
    }
    if (system("/usr/lib/nis/nisping -u 2>/dev/null") == 0) {
	print "Updating NIS+ user home directories.\n" if $opt_v;
	system("/usr/bin/sed 's/\$/p/' <$sedpasswd >${sedpasswd}n");
	if (open NISUSERS, "/usr/lib/nis/nisaddent -d passwd | " .
	    "/usr/bin/sed -n -f ${sedpasswd}n |") {
	    local (@updatedusers) = <NISUSERS>;
	    close NISUSERS;
	    if (@updatedusers) {
		if (open NISUSERS, "| /usr/lib/nis/nisaddent passwd") {
		    foreach $user (@updatedusers) {
			print NISUSERS $user;
		    }
		    close NISUSERS || warn "NIS+ update failure.\n";
		} else {
		    warn "NIS+ update failure.\n";
		}
	    } else {
		print "No NIS+ users need to be updated.\n" if $opt_v;
	    }
	} else {
	    warn "Unable to read NIS+ users.\n";
	}
	unlink $sedpasswd . "n";
    } elsif (($ypmaster = `/usr/bin/ypwhich 2>/dev/null`) && $? == 0) {
	$ypmaster =~ /(.*)\n/;
	($ypmaster) = gethostbyname($1);
	($thishost) = gethostbyname(hostname);
	if ($ypmaster eq $thishost) {
	    system("cd /var/yp && make")
	      if yesno("Rebuild NIS/YP maps", $opt_y);
	} else {
	    warn "Not running on NIS/YP master $1; unable to update user shells\n";
	    print "Use 'sed -f $sedpasswd <$passwd >${passwd}.new' on the master\n";
	    print "and then remake the NIS/YP database.\n";
	    undef $sedpasswd;
	}
    }
    unlink $sedpasswd if $sedpasswd;
}

# Show usage message.
sub usage
{
    print "Usage:\n\n";
    print "\t$0 [-rsvy]\n\n";
    print "    -n - non-interactive mode.\n";
    print "    -r - revert back to aspppd configuration.\n";
    print "    -s - use strict translation.\n";
    print "    -v - print more detail of the operations performed.\n";
    print "    -y - assume 'yes' as default answer where reasonable.\n";
    exit;
}

# Correct an environment variable so that it points at either a useful
# executable program, or nothing at all.
sub fixpath
{
    local ($prog, $deflt) = @_;

    $prog = $deflt if $prog eq "";
    if ($prog !~ /^(\.\/|\/)/) {
	local ($_) = $ENV{PATH};
	$_ = "/bin:/usr/bin:/sbin:/usr/sbin" if $_ eq "";
	split /:/;
	foreach (@_) {
	    $prog = $_ . "/" . $prog, last if -x $_ . "/" . $prog;
	}
    }
    $prog = "" if !(-x $prog);
    $prog;
}

getopts('nrsvy') || usage;

die "Need permission to modify system files.\n"
    unless ($> == 0 || yesno "This script should be run as root.  Continue");

if ($opt_r) {
    local ($intemp);

# Revert to previous configuration.  Just rename the aspppd file back
# and undo changes to the passwd file.

    die "No saved aspppd configuration exists.\n" unless -f $asmoved;
    if (-e $astemp) {
	die "$astemp is not a file\n" unless -f $asfile;
	unlink $astemp || die "Cannot remove temporary $astemp: $!\n";
    }
    $intemp = 0;
    if (-e $asfile) {
	die "$asfile is not a file\n" unless -f $asfile;
	die "Not modifying configuration.\n"
	    unless yesno "Remove existing $asfile", $opt_y;
	rename $asfile, $astemp || die "Cannot rename existing $asfile: $!\n";
	$intemp = 1;
    }

    if (rename $asmoved, $asfile) {
	unlink $astemp || warn "$astemp: $!\n" if $intemp;
    } else {
	$failure = "Cannot rename $asmoved to $asfile: $!\n";
	rename $astemp, $asfile ||
	    die "$failure\nand cannot recover: $!\n" .
		"Saved current asppp.cf in $astemp\n"
		    if $intemp;
	die $failure;
    }

    $( = $);
    $< = $>;

    system($pppdctl, "stop") if -x $pppdctl;
    # remove pppd autostart files.
    unlink $pppdir . "ifconfig";
    unlink $pppdir . "demand";

    system($asctl, "start") if -x $asctl;

    open SEDFILE, ">$sedpasswd" || die "Cannot write $sedpasswd: $!\n";
    local ($escdir) = $pppdir;
    $escdir =~ s+/+\\/+g;
    print SEDFILE "/${escdir}dial-in\\./s+[^:]*\$+/usr/sbin/aspppls+\n";
    print SEDFILE "/\\/usr\\/bin\\/pppd/s+[^:]*\$+/usr/sbin/aspppls+\n";
    close SEDFILE;

    rewrite_passwd;

    exit 0;
}

$aspppcf = $asfile;
if (!(-f $asfile)) {
    die "No aspppd configuration exists; nothing to convert.\n"
	unless -f $asmoved;
    die "No changes made.\n"
	unless yesno "Already converted; rerun anyway";
    $aspppcf = $asmoved;
}

print "This script provides only a suggested translation for your existing aspppd\n";
print "configuration.  You will need to evaluate for yourself whether the translation\n";
print "is appropriate for your operating environment.\n";
die "No changes made.\n"
  unless yesno "Continue", 1;

# Read in the asppp.cf file first; there's no reason to continue on to
# the UUCP files if this file isn't readable or has no paths defined.
local($ifconfig, $paths) = readaspppcf($aspppcf);

# Loop over the ifconfigs and build a list of the down ones.
foreach $intf (keys %$ifconfig) {
    local(@words) = @{$$ifconfig{$intf}};
    while ($word = shift @words) {
	shift @words if $ifconfigtakes{$word};
	if ($word =~ /^down$/) {
	    warn("Why is $intf declared down?\n"), last
		if $intf =~ /^ipd[0-9]+$/;
	    $downif{$intf} = $$ifconfig{$intf};
	    delete $$ifconfig{$intf};
	    last;
	}
    }
}

# Read /etc/passwd for dial-in users configured for aspppd.
local($dialinusers) = readpasswd;

# Read in existing pppd configuration.  All we really care about
# is the setting of the "auth" option.
undef $authoption;
if (open(OPTIONS,"<" . $options)) {
    while (@{$words = uucpline(OPTIONS, $options)}) {
	while ($_ = pop @$words) {
	    $authoption = $_ if /auth/i;
	}
    }
    close OPTIONS;
    $authoption = "unknown" if !defined($authoption);
}

$dialin_auth = 0;
if ($authoption =~ /^auth$/i) {
    $dialin_auth = 1;
} elsif ($authoption =~ /^noauth$/i) {
    $dialin_auth = 2;
} elsif (defined($authoption)) {
    $dialin_auth = 3;
}

# Check that there's a path for each dial in user
foreach $user (keys %$dialinusers) {
    if (!defined($$paths{$user})) {
	warn "Dial-in user ", $user,
	    " does not have a corresponding dial-in path.\n";
	delete $$dialinusers{$user};
	next;
    }
    $intf = ${$$paths{$user}}{"interface"};
    if ($intf eq "ipdptp*") {
	if (0+keys(%downif) == 0) {
	    warn "Dial-in user $path has no available \"down\" interfaces.\n";
	    delete $$dialinusers{$user};
	    next;
	}
    } else {
	if (!defined($downif{$intf}) && !defined($$ifconfig{$intf})) {
	    warn "Dial-in path $user has undefined $intf; deleted.\n";
	    delete $$dialinusers{$user};
	    next;
	}
    }
    ${$$paths{$user}}{$isdialin} = 1;
# 0 - no info (no options file, "noauth" on call)
# 1 - all auth ("auth" in options, "noauth" on call)
# 2 - all noauth ("noauth" in options)
# 3 - mixed; use auth ("noauth" in options, wrapper script for "auth")
    if (${$$paths{$user}}{require_authentication}) {
	if ($dialin_auth == 2) {
	    $dialin_auth = 3;
	} elsif ($dialin_auth == 0) {
	    $dialin_auth = 1;
	}
    } else {
	if ($dialin_auth == 1) {
	    $dialin_auth = 3;
	} elsif ($dialin_auth == 0) {
	    $dialin_auth = 2;
	}
    }
}

# Get lists of usable dial-in and dial-out ports.
local($dialin,$dialout) = getserialports;

# Read and parse the UUCP Sysfiles, Devconfig, and Limits files.
# These are keyed with the "service=" string.  The Sysfiles file can
# augment or override the list of files read for a given service.
print "Reading UUCP configuration.\n" if $opt_v;
@sysfiles = @{${uucpkeyfile($Sysfiles,"service=")}{"ppp"}};
@limits = @{${uucpkeyfile($Limits,"service=")}{"ppp"}};
%devconfig = %{uucpkeyfile($Devconfig,"service=ppp","device=")};

# Now read in the UUCP files corresponding to this service.
$systems = uucpposfiles(uucpfiles("systems"));
$dialers = uucpposfiles(uucpfiles("dialers"));
$dialcodes = uucpposfiles($Dialcodes);
$devices = uucpdevices(uucpfiles("devices"));

# just to make sure
$$dialcodes{""} = ();

# Loop over paths.  Dial-out only paths are translated into demand-dial
# configurations.  Dial-in only paths are translated into appropriate
# log-in entries.
local (@bidirectional);
foreach $peer (keys %$paths) {
    if (exists($$systems{$peer})) {
	$sline = $$systems{$peer};
	if ($$sline[0] eq "Never") {
	    if (${$$paths{$peer}}{$isdialin}) {
		translatedialin($peer);
	    } else {
		print "We never call $peer, and he never calls us.\n"
		    if $opt_v;
	    }
	    delete $$paths{$peer};
	    next;
	}
	push @bidirectional, $peer if ${$$paths{$peer}}{$isdialin};
	print "Ignoring time restriction on $peer\n"
	    if $$sline[0] ne "Any";
	$dlist = $$devices{$$sline[1]};
	$class = $$sline[2];
	$i = 0;
	while ($i < @$dlist) {
	    local($dev) = $$dlist[$i];
	    if ($$dev[1] ne "-") {
		print "Ignoring device $$dev[0]; 801-type not supported.\n";
		splice @$dlist, $i, 1;
		next;
	    }
	    $i++;

	    # Make sure that classes match.
	    next if $$dev[2] ne "Any" && $class ne "Any" && $$dev[2] ne $class;
	    # Prepend "/dev/" if it's not present in the device name.
	    if (exists($$dialout{$$dev[0]})) {
		# This just seems odd.
		$dname = $$dialout{$$dev[0]};
		$dname =~ s+/dev/term/+/dev/cua/+;
	    } else {
		$dname = ($$dev[0] =~ m+^/+ ? $$dev[0] : ("/dev/" . $$dev[0]));
	    }
	    # Skip devices that aren't supposed to be used for dial-out.
	    next if $dname =~ m+^/dev/term/+;
	    next if $dname =~ m+^/dev/tty[a-z]$+;
	    # Make sure this is a character device and we have access to it.
	    next unless -w $dname && -c $dname;
	    warn "Dialer for $$dev[3] is missing.\n"
		unless exists($warned{$$dev[3]}) ||
		    exists($$dialers{$$dev[3]});
	    $warned{$$dev[3]} = 1;

	    # Expand keywords from Dialcodes file.  Should have \T or \D.
	    $phone = $$sline[3];
	    $xphone = ($$dev[4] eq "\\T" && $phone =~ /^([A-Za-z]*)(.*)$/ ?
	        "@{$$dialcodes{$1}}" . $2 : $phone);

	    # Make a copy of the dialing script.
	    local(@dials) = @{$$dialers{$$dev[3]}};

	    # Translate dial tone and wait characters from Dialers file.
	    $_ = shift @dials;
	    s[(.)(.)]{
		local($from,$to) = ($1,$2);
		$phone =~ s+(^|[^\\])$from+$1$to+gx;
		$xphone =~ s+(^|[^\\])$from+$1$to+gx;
	    }ge;

	    # Translate escapes in dial specification.  Chat has a \T,
	    # but uses \U instead of \D.
	    local($needt, $needu, $isexpect, @chats) = ("", "", 1);
	    foreach $str (@dials) {
		push(@chats, "") if $str eq "";
		local ($ostr) = "";
		if ($isexpect) {
		    while ($str =~ s/([^\\]*)\\(.)//) {
			local($lead, $_) = ($1, $2);
			/[Mm]/ ? ($ostr .= $lead) :
			/[Ee]/ ? ($sorrye = 1, $ostr .= $lead) :
			($ostr .= $lead . "\\" . $_);
		    }
		} else {
		    while ($str =~ s/([^\\]*)\\(.)//) {
			local($lead, $_) = ($1, $2);
			/T/ ? ($needt = " -T '$xphone'",
			       $ostr .= $lead . "\\T") :
			/D/ ? ($needu = " -U '$phone'",
			       $ostr .= $lead . "\\U") :
			/M/ ? ($ostr .= $lead,
			       ($ostr ne "" ? push(@chats, $ostr, "\\c"):0),
			       push(@chats, "HANGUP", "OFF"), $ostr = "") :
			/m/ ? ($ostr .= $lead,
			       ($ostr ne "" ? push(@chats, $ostr, "\\c"):0),
			       push(@chats, "HANGUP", "ON"), $ostr = "") :
			/[Ee]/ ? ($sorrye = 1, $ostr .= $lead) :
			/[dp]/ ? ($ostr .= $lead . "\\" . $_ . "\\" . $_) :
			/c/ ? ($str eq "" ? ($ostr .= $lead . "\\c") : 0) :
			($ostr .= $lead . "\\" . $_);
		    }
		}
		$ostr .= $str;
		push @chats, $ostr if $ostr ne "";
		$isexpect = !$isexpect;
	    }

	    # Pad out dial list if we're missing a "send" string and tack
	    # on the chat list from the Systems file.
	    if (defined $$sline[4]) {
		push @chats, "\\c" if !$isexpect;
		push @chats, (splice @$sline, 4);
	    }

	    $chatfile = $pppdir . "chat.$peer.$$dev[3]";
	    if (-e $chatfile) {
		print "$chatfile already exists.\n";
		if (!yesno("Should it be overwritten",$opt_y)) {
		    if (yesno("Should it be used as-is")) {
			warn "Using $chatfile as-is; it may not be correct.\n";
		    } else {
			for ($n = 0; ; $n++) {
			    last if !(-e $chatfile . "." . $n);
			}
			$chatfile .= "." . $n;
			print "Using $chatfile instead.\n";
			$chatfiles{$chatfile} = \@chats;
		    }
		} else {
		    $overwrite{$chatfile} = 1;
		    $chatfiles{$chatfile} = \@chats;
		}
	    } else {
		$chatfiles{$chatfile} = \@chats;
	    }

	    push @pppdargs, $dname;
	    push @pppdargs, $class if $class =~ /^[0-9]+$/;
	    push @pppdargs, "demand";
	    convert_options(\@pppdargs,$$paths{$peer},
		$chatfile . $needt . $needu, undef);

	    $optname = $peersdir . $peer;
	    if (-e $optname) {
		print "$optname already exists.\n";
		if (!yesno("Should it be overwritten", $opt_y)) {
		    if (yesno("Should it be used as-is")) {
			warn "Using $optname as-is; it may not be correct.\n";
		    } else {
			for ($n = 0; ; $n++) {
			    last if !(-e $optname . "." . $n);
			}
			$optname .= "." . $n;
			print "Using $optname instead.\n";
			$optfiles{$optname} = \@pppdargs;
		    }
		} else {
		    $overwrite{$optname} = 1;
		    $optfiles{$optname} = \@pppdargs;
		}
	    } else {
		$optfiles{$optname} = \@pppdargs;
	    }
	    $scriptfiles{$pppdir . "demand"} .= "/usr/bin/pppd file $optname\n";
	    last;
	}
    } elsif (${$$paths{$peer}}{$isdialin}) {
    	translatedialin($peer);
    } else {
	warn "Path $peer has no dial-in user nor Systems file entry.\n";
	delete $$paths{$peer};
    }
}

warn "Chat cannot do echo checking; requests for this removed.\n" if $sorrye;

if (@bidirectional) {
    print "\nWarning:  The following paths are bidirectional:\n";
    print "\t@bidirectional\n\n";
    print "Bidirectional paths (with entries in both Systems and passwd) do not translate\n";
    print "into Solaris PPP 4.0 semantics in an exact manner.  The dial-out portion will\n";
    print "use the designated interface, but the dial-in portion will use any available\n";
    print "interface.\n";
    while ($peer = pop @bidirectional) {
	delete $ {$$paths{$peer}}{interface};
	translatedialin($peer);
    }
}

translateifconfig;

# Create an /etc/ppp/options if we need to.
if (!defined($authoption) && $dialin_auth > 0) {
    local (@pppdopts);
    push @pppdopts, "lock";
    push @pppdopts, "auth" if $dialin_auth == 1;
    push @pppdopts, "noauth" if $dialin_auth > 1;
    $optfiles{$options} = \@pppdopts;
}
# Translate option files to plain text.
foreach $file (keys %optfiles) {
    local ($opts) = $optfiles{$file};
    local ($cstr) = "";
    $cstr .= shift(@$opts) . "\n" while @$opts;
    $optfiles{$file} = $cstr;
}
# Change "auth" to "noauth" or add "noauth" to /etc/ppp/options.
if (defined($authoption) && $authoption ne "noauth" && $dialin_auth == 3) {
    local(@triplet, $cstr);
    if ($authoption eq "unknown") {
	warn "Adding 'noauth' to $options\n";
    } else {
	warn "Changing 'auth' in $options to 'noauth'\n";
    }
    open(OPTIONS,"<" . $options) || die "$options disappeared: $!\n";
    while (@{$words = uucpline(OPTIONS, $options, \@triplet)}) {
	$cstr .= $triplet[0];
	if (grep(/auth/, @$words)) {
	    local(@newwords) = map { $_ = "noauth" if /auth/; $_ } @$words;
	    $cstr .= "@newwords";
	} else {
	    $cstr .= $triplet[1];
	}
	while (pop @$words) {
	    $authoption = $_ if /auth/i;
	}
	$cstr .= $triplet[2];
    }
    $cstr .= $triplet[0] . $triplet[2];
    close OPTIONS;
    $cstr .= "\n" if $cstr !~ /\n$/;
    $cstr .= "noauth\n" if $authoption eq "unknown";
    $optfiles{$options} = $cstr;
}

# Create a sed script to fix the users' shell paths.
if (0+(keys %dialinshell) != 0) {
    $cstr = "";
    foreach $peer (keys %dialinshell) {
	$cstr .= "/^$peer:/s+[^:]*/aspppls\$+$dialinshell{$peer}+\n";
    }
    $scriptfiles{$sedpasswd} = $cstr;
}

print "\nPreparing to write out translated configuration:\n";

# Enumerate the files we'll write.
$nfiles = 0;
if (0+(keys %chatfiles) != 0) {
    print "    ";
    nof 0+(keys %chatfiles), "chat file", ":\n";
    foreach $file (keys %chatfiles) {
	$nfiles++;
	print "\t$nfiles.  $file\n";
	local ($chats) = $chatfiles{$file};
	local ($cstr) = "";
	while (@$chats) {
	    $cstr .= requote(shift(@$chats));
	    $cstr .= " " . requote(shift(@$chats)) if @$chats;
	    $cstr .= "\n";
	}
	local (@filerec) = ( $file, $cstr );
	push @allfiles, \@filerec;
    }
}
if (0+(keys %optfiles) != 0) {
    print "    ";
    nof 0+(keys %optfiles), "option file", ":\n";
    foreach $file (keys %optfiles) {
	$nfiles++;
	print "\t$nfiles.  $file\n";
	local (@filerec) = ( $file, $optfiles{$file} );
	push @allfiles, \@filerec;
    }
}
if (0+(keys %scriptfiles) != 0) {
    print "    ";
    nof 0+(keys %scriptfiles), "script file", ":\n";
    foreach $file (keys %scriptfiles) {
	$nfiles++;
	print "\t$nfiles.  $file\n";
	local (@filerec) = ( $file, $scriptfiles{$file} );
	push @allfiles, \@filerec;
    }
}

# Merge new secrets needed with existing ones, if any.
sub merge_secrets
{
    local ($addsecrets, $fname) = @_;
    local ($file, $cstr, @triplet, $newsecret);

    $nfiles++;
    $file = $pppdir . $fname;
    print "\t$nfiles.  $file\n";
    if (open(SECRETS, '<' . $pppdir . $fname)) {
	while (@{$words = uucpline(SECRETS, $pppdir . $fname, \@triplet)}) {
	    $cstr .= $triplet[0];
	    $newsecret = $ {$$addsecrets{$$words[0]}}{$$words[1]};
	    if (defined $newsecret) {
		$cstr .= requote($$words[0]) . " " . requote($$words[1]) .
		  " " . $newsecret;
		delete $ {$$addsecrets{$$words[0]}}{$$words[1]};
	    } else {
		$cstr .= $triplet[1];
	    }
	    $cstr .= $triplet[2];
	}
	close SECRETS;
	$cstr .= $triplet[0] . $triplet[2];
    }
    foreach $key1 (keys (%$addsecrets)) {
	foreach $key2 (keys (%{$$addsecrets{$key1}})) {
	    $cstr .= requote($key1) . " " . requote($key2) . " " .
	      $ {$$addsecrets{$key1}}{$key2} . "\n";
	}
    }
    local (@filerec) = ( $file, $cstr );
    push @allfiles, \@filerec;
}

$nchap = 0+(keys %chapsecrets) != 0;
$npap = 0+(keys %papsecrets) != 0;
if ($nchap != 0 || $npap != 0) {
    print "    ";
    nof $nchap + $npap, "secrets file", ":\n";
    merge_secrets(\%chapsecrets, "chap-secrets") if $nchap != 0;
    merge_secrets(\%papsecrets, "pap-secrets") if $npap != 0;
}

die "Nothing to write back; I'm done.\n" if $nfiles == 0;

$PAGER = fixpath($ENV{PAGER}, "/usr/bin/less");
$EDITOR = fixpath($ENV{EDITOR}, "/usr/bin/vi");
$SHELL = fixpath($ENV{SHELL}, "/usr/bin/ksh");

END {
    if ($tempname) {
	unlink($tempname) or
	    die "Cannot remove temporary file $tempname: $!\n";
    }
}

sub show_file_options
{
    print "\nEnter option number:\n";
    print "\t1 - view contents of file on standard output\n";
    print "\t2 - view contents of file using $PAGER\n" if $PAGER ne "";
    print "\t3 - edit contents of file using $EDITOR\n" if $EDITOR ne "";
    print "\t4 - delete/undelete file from list\n";
    print "\t5 - rename file in list\n";
    print "\t6 - show file list again\n";
    print "\t7 - escape to shell (or \"!cmd\")\n";
    print "\t8 - abort without saving anything\n";
    print "\t9 - save all files and exit (default)\n";
}

# If interactive, then allow user to view and modify converted data.
if ((-t STDIN) && (-t STDOUT) && !$opt_n) {
    show_file_options();
    while (1) {
	print "Option:  ";
	chomp($ans = <STDIN>);
	if ($ans eq "?" || $ans =~ /^h/i) {
	    show_file_options();
	    next;
	}
	if ($ans eq "") {
	    last if yesno "Saving all files.  Are you sure";
	    next;
	}
	last if $ans == 9;
	print("Aborted.\n"), exit if $ans == 8;
	if ($ans =~ /^!/ || $ans == 7) {
	    if ($ans =~ /^!(.+)/) {
		system($1);
	    } else {
		print("Interactive shell access not permitted here.\n"), next
		    if $< != $>;
		system($SHELL);
	    }
	} elsif ($ans == 6) {
	    for ($i = 0; $i < $nfiles; $i++) {
		print "\t", $i+1, ".  $allfiles[$i][0]",
		    ($deleted[$i] ? "   (deleted)" : ""), "\n";
	    }
	} elsif ($ans > 0 && $ans < 6) {
	    $fnum = 0;
	    if ($nfiles > 1) {
		print "File number (1 .. $nfiles):  ";
		chomp($fnum = <STDIN>);
		if ($fnum < 1 || $fnum > $nfiles) {
		    print "Unknown file (must be 1 to $nfiles).\n";
		    next;
		}
		$fnum--;
	    }
	    if ($ans == 5) {
		print "Current name is $allfiles[$fnum][0]\n";
		print "New name:  ";
		chomp($fname = <STDIN>);
		print("Unchanged\n"), next if $fname eq "";
		$allfiles[$fnum][0] = $fname;
	    }
	    if ($deleted[$fnum]) {
		if (yesno("File " . $fnum+1 .
		   " ($allfiles[$fnum][0]) is deleted; undelete",1)) {
		    undef $deleted[$fnum];
		}
		next;
	    }
	    if ($ans == 1) {
		print $allfiles[$fnum][1];
	    } elsif ($ans == 2 && $PAGER ne "") {
		$i = 0;
		do {
		    if (++$i > 5) {
			warn "Unable to open temporary file: $!";
			undef $tempname;
			last;
		    }
		    $tempname = tmpnam();
		} until sysopen(FH, $tempname, O_RDWR|O_CREAT|O_EXCL);
		next if !$tempname;
		print FH $allfiles[$fnum][1];
		close FH;
		system($PAGER, $tempname);
		unlink($tempname) ||
		    warn "Trouble removing temporary file: $!";
		undef $tempname;
	    } elsif ($ans == 3 && $EDITOR ne "") {
		$i = 0;
		do {
		    if (++$i > 5) {
			warn "Unable to open temporary file: $!";
			undef $tempname;
			last;
		    }
		    $tempname = tmpnam();
		} until sysopen(FH, $tempname, O_RDWR|O_CREAT|O_EXCL);
		next if !$tempname;
		chown $<, $(, $tempname;
		print FH $allfiles[$fnum][1];
		close FH;
		$i = system($EDITOR, $tempname);
		if ($i == 0) {
		    if (open FH, "<" . $tempname) {
			read FH, $allfiles[$fnum][1], (-s $tempname);
			close FH;
		    }
		} else {
		    print "Editor dropped core.\n" if $? & 128;
		    print "Editor terminated on signal ", $? & 127, "\n"
			if $? & 127;
		    print "Editor returned error ", $? >> 8, "\n"
			if $? >> 8;
		}
		unlink($tempname) ||
		    warn "Trouble removing temporary file: $!";
		undef $tempname;
	    } elsif ($ans == 4) {
		$deleted[$fnum] = 1;
	    }
	}
    }
}

print "\n";

# Interactive part is over.  Become real.
$( = $);
$< = $>;

print "Stopping aspppd\n" if $opt_v;
system($asctl, "stop") if -x $asctl;

print "Saving all files\n" if $opt_v;
for ($i = 0; $i < $nfiles; $i++) {
    $filerec = $allfiles[$i];
    if ($deleted[$i]) {
	delete $scriptfiles{$$filerec[0]};
	next;
    }
    print "Saving $$filerec[0]\n" if $opt_v;
    $$filerec[0] =~ m+(.*)/+;
    if ($1 eq "") {
	# this is ok; just a top level file
    } elsif (!(-d $1)) {
	local ($exdir) = $1;
	while ($exdir && !(-d $exdir)) {
	    $exdir =~ m+(.*)/+;
	    $exdir = $1;
	}
	if ($exdir) {
	    local ($dir) = $1;
	    $dir =~ m+$exdir/([^/]*)(.*)+;
	    local ($tomake, $rest) = ($1, $2);
	    mkdir $exdir . "/" . $tomake, 0775;
	    if ($! == ENOSYS) {
		warn "Unable to make directory $exdir/$tomake; automount point.\n";
		next;
	    }
	    if ($! != 0) {
		warn "Unable to make directory $exdir/$tomake: $!\n";
		next;
	    }
	    if (system("mkdir", "-p", $dir) != 0) {
		warn "Failed to make $dir\n";
		next;
	    }
	} else {
	    warn "$1 doesn't appear to have a useful path.\n";
	    next;
	}
    }
    undef $fileerr;
    local ($fname) = $$filerec[0];
    if (-e $fname && !$overwrite{$chatfile}) {
	print "$fname already exists.\n"
	  if (-t STDIN) && (-t STDOUT) && !$opt_n;
	if (!yesno("Should it be overwritten",$opt_y)) {
	    warn "Using $fname as-is; it may not be correct.\n";
	    next;
	}
    }
    if (sysopen(OUTFILE, $$filerec[0], O_WRONLY|O_CREAT|O_TRUNC, 0600)) {
	print OUTFILE $$filerec[1] || ($fileerr = $!);
	close OUTFILE || ($fileerr = $!);
    } else {
	$fileerr = $!;
    }
    warn "Unable to write $$filerec[0]: $fileerr\n" if $fileerr;
}

local(@scripts) = keys %scriptfiles;
if (@scripts) {
    print "Making scripts executable\n" if $opt_v;
    system("chmod", "u+x", @scripts);
}

rewrite_passwd if exists($scriptfiles{$sedpasswd});

# clean up after a previous translation.
unlink $pppdir . "ifconfig" if !$scriptfiles{$pppdir . "ifconfig"};
unlink $pppdir . "demand" if !$scriptfiles{$pppdir . "demand"};

(rename($asfile, $asmoved) || warn "Cannot move $asfile: $!\n")
  if $aspppcf ne $astemp;

system($pppdctl, "start") if -x $pppdctl;

# use Dumpvalue;
# my $dumper = new Dumpvalue;
# $dumper->set(globPrint => 1);
# $dumper->dumpValue($ifconfig);