#!/usr/bin/env perl
#-*- Mode: perl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-

# Simple shares configurator. Designed to be architecture- and distribution independent.
#
# Version 0.1.0 - copyright (C) 2000-2001 Ximian, Inc.
#
# Authors: Hans Petter Jansson <hpj@ximian.com>
#          Michael Vogt <mvo@debian.org> - Debian 2.[2|3] support.
#          David Lee Ludwig <davidl@wpi.edu> - Debian 2.[2|3] support.
#
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Library General Public License as published
# by the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

# Best viewed with 100 columns of width.

# WARNING: Works with Red Hat (and probably most derivates) and SuSE.
# You need to have Samba installed. We're more than happy to receive patches
# for other distributions.

# Configuration files affected:
#
# /etc/sysconfig/network
# /etc/sysconfig/network-scripts/ifcfg-*
# /etc/network/interfaces
# /etc/network/options
# /etc/rc.config
# /etc/fstab
# /etc/smb.conf

# Running programs affected/used:
#
# ifconfig
# ping
# showmount
# nmblookup
# smbclient


require "/usr/share/ximian-setup-tools/scripts/general.pl";
require "/usr/share/ximian-setup-tools/scripts/platform.pl";
require "/usr/share/ximian-setup-tools/scripts/util.pl";
require "/usr/share/ximian-setup-tools/scripts/file.pl";
require "/usr/share/ximian-setup-tools/scripts/xml.pl";
require "/usr/share/ximian-setup-tools/scripts/service.pl";
require "/usr/share/ximian-setup-tools/scripts/network.pl";


# --- Tool information --- #

$name = "shares";
$version = "0.1.0";
@platforms = ("redhat-5.2", "redhat-6.0", "redhat-6.1", "redhat-6.2", "redhat-7.0");

$description =<<"end_of_description;";
       Configures network imports and exports.
end_of_description;

$progress_max = 8;


# --- System config file locations --- #

# We list each config file type with as many alternate locations as possible.
# They are tried in array order. First found = used.

# Right now there's only one entry per array, as I couldn't find any
# typical deviations.

@sysconfig_network_names =  ( "/etc/sysconfig/network" );
@network_interfaces_names = ( "/etc/network/interfaces" );
@network_options_names =    ( "/etc/network/options" );
@rc_config_names =          ( "/etc/rc.config" );
@fstab_names =              ( "/etc/fstab" );
@exports_names =            ( "/etc/exports" );
@smb_conf_names =           ( "/etc/smb.conf", "/etc/samba/smb.conf" );


# --- Internal configuration variables --- #

# Configuration is parsed/read to, and printed/written from, these temporary variables.

$cf_hostip = "";
$cf_netmask = "";
$cf_workgroup = "";
@cf_shares_list = ();
@cf_exports_list = ();

$cf_interface = "";
$cf_netmask = "";
$cf_hostip = "";


# --- Configuration file manipulation --- #

# Debian 2.[2|3]+ style /etc/network/interfaces
#
# <&filtered lines>
# iface <interface> inet <static|dhcp|bootp|ppp>
#     address <ip>
#     netmask <ip mask>
#     gateway <gateway ip>
#     broadcast <broadcast ip>
#     network <network ip>
#     noauto (this is only used to not enable the interface on boot)
#     up <command to run when the interface is brought up>
#     down <command to run when the interface is brought down>
# <&filtered lines>
# (more interfaces may follow)
#
# A "#" character at the very beginning of a line causes that line to be
# treated as a comment.
# A "\" character at the very end of a line causes that line to continue onto
# the next line.
#
# &read_network_interfaces_into_hash will read the contents of
# /etc/network/interfaces into a hash table of references to hash tables.  For
# example:
#
# %ifaces = ("eth0" => {"method" => "dhcp"},
#            "eth1" => {"method" => "static",
#                       "address" => "192.168.0.42",
#                       "netmask" => "255.255.255.0"});
#
# The resulting hash table, %ifaces, is returned.
#
# NOTE: For more information on the format of /etc/network/interfaces, read
# the "interfaces" man page (section 5).  Also be sure to look at the example
# file in /usr/share/doc/netbase/examples/interfaces.  Both of these are
# included as part of the "netbase" package in Debian 2.[2|3].
#
# Exists: Debian 2.[2|3]
#
# Absent:

sub read_network_interfaces_into_hash
{
  local *FILE;
  my (%ifaces, $current_name, $current_iface);

  *FILE = &xst_open_read_from_names(@network_interfaces_names);
  if (not *FILE) { return; }

  # Build a list of all interfaces
  while (<FILE>)
  {
    # Split the current line into a list of tokens.
    @line = split(/[ \n\r\t]+/, $_);

    # Remove leading whitespace.
    if ($line[0] eq "") { shift(@line); }

    # Check to see if this line continues onto other lines.  If so, append
    # them
    while ($line[$#line] eq "\\")
    {
      # Remove the \ character.
      pop @line;

      # Read in the next line and split it into a list of tokens.
      @nextline = split(/[ \n\r\t]+/, <FILE>);

      # Remove any leading whitepage.
      if ($nextline[0] eq "") { shift(@nextline); }

      # Append @nextline to the end of @line
      push @line, @nextline;
    }

    # Make sure this line isn't a comment.
    if (&xst_ignore_line($line[0])) { next; }

    # Check if the line specifies a new interface.
    if ($line[0] eq "iface" && not &xst_ignore_line($line[1]))
    {
      # Make sure the device was detected via ifconfig (see "&get_immediate").
      # NOTE: This will weed out any loopback or dummy interfaces.
      if (not &xst_item_is_in_list($line[1], @cf_interface_list))
      {
        &xst_report_warning(3, "$line[1] isn't an available, configured interface");
        next;
      }

      # Make sure the interface is &set up for IPv4/internet networking
      if ($line[2] ne "inet") { next; }

      # Also make sure there is a &set configuration method.
      if ($line[3] eq "" || &xst_ignore_line($line[3]))
      {
        &xst_report_warning(4, "Interface $line[1] has no configuration method");
        next;
      }

      # Create a new hash table for this interface.
      my %iface_hash = ();

      # Set this interface to be the 'current' one.
      $current_name = $line[1];
      $current_iface = $ifaces{$current_name} = \%iface_hash;

      # Set the interface's method
      $$current_iface{"method"} = $line[3];
    }
    else  # Add an attribute to an already &set interface.
    {
      # Make sure an interface has been &set.
      if (not $current_iface) { next; }

      # Make sure the attribute has been properly &set
      if (not $line[1]) { next; }

      # Set the attribute
      $$current_iface{$line[0]} = join(' ', $line[1..$#line]);
    }
  }

  # All done.  Clean up before returning.
  close($FILE);

  return %ifaces;
}


# Debian 2.[2|3]+ style /etc/network/interfaces
#
# (See '&read_network_interfaces_into_hash()' for information on
# /etc/network/interfaces as well as the structure of %ifaces.
#
# Takes network interface information and places them into the correct
# variables.
#
# Exists: Debian 2.[2|3]
#
# Absent:

sub read_network_interfaces
{
  my (%ifaces, $current_name, $current_iface);
  my $chosen_name;

  %ifaces = &read_network_interfaces_into_hash();

  # Now, a "primary" interface will be selected.  The selection order is as
  # follows:
  #
  # * If only one interface exists, use that one.
  # * If an interface has a preset gateway, use that one.
  # * The first interface with a dynamic configuration "method" (such as "dhcp",
  #   "bootp", "ppp", etc.) will be selected.
  # * If all else fails, pick the first interface.

  # See if only one interface exists.
  if ( scalar(keys(%ifaces)) == 1 )
  {
    $chosen_name = (keys(%ifaces))[0];

    &xst_report_info(9, "Only one interface, $chosen_name, exists. It is being " .
                      "selected as the primary interface");
  }

  # For that matter, do any interfaces exist at all.
  elsif ( scalar(keys(%ifaces)) == 0 )
  {
    &xst_report_warning(5, "No network interfaces found");
    return;
  }

  else
  {
    # Look for an interface with a preset gateway.
    $chosen_name = &xst_get_key_for_subkeys(\%ifaces, ["gateway"]);

    # If need be, look for an interface with a dynamic configuration "method".
    if ( $chosen_name eq "" )
    {
      $chosen_name = &xst_get_key_for_subkey_and_subvalues(\%ifaces, "method", ["dhcp", "bootp", "ppp"]);
    }

    # If an interface hasn't been chosen, then just pick the first available one.
    if ( $chosen_name eq "" )
    {
      $chosen_name = (keys(%ifaces))[0];
    }
  }

  # Now that a "primary" interface has been chosen, &set the proper global
  # variables.
  %current_iface = %{$ifaces{$chosen_name}};

  $cf_interface = $chosen_name;
  $cf_hostip = $current_iface{"address"};
  $cf_netmask = $current_iface{"netmask"};
  $cf_gateway = $current_iface{"gateway"};
  $cf_method = $current_iface{"method"};
  if ( exists $current_iface{"noauto"} ) { $cf_onboot = 0; }
  else { $cf_onboot = 1; }
}

# Red Hat style /etc/sysconfig/network-scripts/ifcfg-*
#
# <&filtered lines>
# IPADDR=<ip>
# NETMASK=<ip mask>
# NETWORK=<network ip>
# BROADCAST=<broadcast ip>
# BOOTPROTO=<bootp|dhcp|none>
# ONBOOT=<boolean>
# <&filtered lines>
#
# Determines the configuration of a specific network interface. First
# argument must be the name of the interface.
#
# Exists: Red Hat [5|6].x
#
# Absent:

sub read_sysconfig_network_scripts_ifcfg
{
  local *FILE;

  # Find the file.

  *FILE = &xst_open_read_from_names("/etc/sysconfig/network-scripts/ifcfg-$_[0]");
  if (not *FILE) { return; }

  # Parse the file.

  while (<FILE>)
  {
    @line = split(/[ \n\r\t\"\'=]+/, $_);
    if ($line[0] eq "") { shift(@line); }  # Leading whitespace. He.

    if ($line[0] eq "IPADDR" && not &xst_ignore_line($line[1]))
    { $cf_hostip = $line[1]; }
    elsif ($line[0] eq "NETMASK" && not &xst_ignore_line($line[1]))
    { $cf_netmask = $line[1]; }
  }

  close(FILE);
}


# Red Hat style primary interface reader.
#
# Tries to figure out what the "primary" interface is, and calls
# &read_sysconfig_network_scripts_ifcfg on that interface.
#
# Must run after &get_immediate and &read_sysconfig_network, which will
# (hopefully) provide information to identify the primary device.

sub read_primary_interface
{
  # If the interface was specified, we use that.

  if ($cf_interface ne "") { }

  # If the known-interfaces list contains only one interface, we use that.
  # This is actually not too uncommon, after weeding lo and dummy.

  elsif ($#cf_interface_list == 0) { $cf_interface = $cf_interface_list[0]; }
  
  # If we have a gateway, try to find an interface with a matching subnet, by
  # going through the interface configurations. This will fail if a gateway
  # has been configured without any interfaces, or if the gateway is on a
  # foreign, bridged subnet. Both are pretty rare conditions.

  elsif ($cf_gateway ne "")
  {
    for $elem (@cf_interface_list)
    {
      &read_sysconfig_network_scripts_ifcfg($elem);
      if ($cf_netmask eq "") { next; }

      if (&ip_calc_network($cf_gateway, $cf_netmask) eq &ip_calc_network($cf_hostip, $cf_netmask))
      {
        $cf_interface = $elem;
        last;
      }

      $cf_hostip = "";
      $cf_netmask = "";
      $cf_method = "";
      $cf_onboot = 0;
    }
  }

  # Last ditch. Pick an interface from the list according to some built-in
  # rules. ethN on Linux and hmeN or Solaris.

  else
  {
    for $elem (@cf_interface_list)
    {
      if (($elem =~ /eth.*/) || ($elem =~ /hme.*/))
      {
        $cf_interface = $elem;
        last;
      }
    }
  }

  if ($cf_interface ne "") { &read_sysconfig_network_scripts_ifcfg($cf_interface); }
}


# Red Hat style /etc/sysconfig/network
#
# <&filtered lines>
# NETWORKING=<boolean>
# FORWARD_IPV4=<boolean>
# GATEWAY=<ip>
# GATEWAYDEV=<interface>
# <&filtered lines>
#
# Determines the primary network configuration. BEWARE: This is actually a
# sourced shell script. We rely on some lenience from the user (and the distro)
# to be able to parse it correctly.
#
# Exists: Red Hat [5|6].x, Caldera 2.4, TurboLinux 6.0, Mandrake 7.0
#
# Absent: SuSE 6.3, SunOS 5.7

sub read_sysconfig_network
{
  local *FILE;

  # Find the file.

  *FILE = &xst_open_read_from_names(@sysconfig_network_names);
  if (not *FILE) { return; }  # We didn't find it.

  # Parse the file.

  while (<FILE>)
  {
    @line = split(/[ \n\r\t\"\'=]+/, $_);
    if ($line[0] eq "") { shift(@line); }  # Leading whitespace. He.

    if ($line[0] eq "GATEWAY" && not &xst_ignore_line($line[1]))
    { $cf_gateway = $line[1]; }
    elsif ($line[0] eq "GATEWAYDEV" && not &xst_ignore_line($line[1]))
    { $cf_interface = $line[1]; }
  }

  close(FILE);
}


# SuSE style /etc/rc.config
#
# <&filtered lines>
# FQHOSTNAME="<fully qualified hostname>"
# SEARCHLIST="<space separated list of searchdomains>"
# NAMESERVER="<space separated list of nameservers>"
# <&filtered lines>
#
# Determines the local hostname.domain, searchdomains and nameservers.
# BEWARE: This is actually a sourced shell script. We rely on some lenience
# from the user (and the distro) to be able to parse it correctly. The file
# is read by SuSE configuration tools and translated to NET-3 config files
# at strategic times.
#
# Exists: SuSE 6.3
#
# Absent: Red Hat 6.x, Caldera 2.4, TurboLinux 6.0, Mandrake 7.0, SunOS 5.7

sub read_rc_config
{
  local *FILE;

  # Find the file.

  *FILE = &xst_open_read_from_names(@rc_config_names);
  if (not *FILE) { return; }  # We didn't find it.

  # Parse the file.

  while (<FILE>)
  {
    @line = split(/[ \n\r\t\"\'=]+/, $_);  # Handles quoted arguments.
    if ($line[0] eq "") { shift(@line); }  # Leading whitespace. He.

    if ($line[0] eq "NETDEV_0" && not &xst_ignore_line($line[1]))
    {
      $cf_interface = $line[1];
    }
    elsif ($line[0] eq "IPADDR_0")
    {
      $cf_hostip = $line[1];
    }
    elsif ($line[0] eq "IFCONFIG_0")
    {
      shift @line;

      while (@line)
      {
        if (&xst_ignore_line($line[0])) { last; }
	
	if ($line[0] eq "broadcast")
	{
	  # Calculate this ourselves.

	  shift @line;
	  shift @line;
	}
	elsif ($line[0] eq "netmask")
	{
	  $cf_netmask = $line[1];
	  shift @line;
	  shift @line;
	}
	elsif ($line[0] eq "bootp")
	{
	  last;
	}
	elsif ($line[0] =~ /dhcp.*/)
	{
	  last;
	}
        elsif ($line[0] ne "") { $cf_hostip = $line[0]; shift @line; }
	else { shift @line; }
      }
    }
  }

  close(FILE);
}


# Samba /etc/smb.conf
#
# <&filtered lines>
# workgroup = <workgroup>
# server string = <description>
# <&filtered lines>
#
# Exists: (Wherever Samba is installed)
#
# Absent: (Wherever Samba is not installed)

sub read_smb_conf
{
  local *FILE;
  my $section = "";
  my $path = "", $enabled = 1, $public = 0, $browseable = 0, $comment = "",
     $writeable = 0, $printable = 0;

  # Find the file.

  *FILE = &xst_open_read_from_names(@smb_conf_names);
  if (not *FILE) { return; }  # We didn't find it.

  # Parse the file.

  while (<FILE>)
  {
    @line = split(/[ \n\r\t=]+/, $_);
    if ($line[0] eq "") { shift(@line); }  # Leading whitespace. He.

    if (join(' ', @line) =~ /^\[.*\]/)
    {
      # New section. If last section was a share, store it in global
      # records, then flush locals for next instance.

      if (($path ne "") && !$printable)
      {
        push @cf_exports_list, "smb";        # Type
        push @cf_exports_list, $path;        # Path
        push @cf_exports_list, $section;     # Name
        push @cf_exports_list, $comment;     # Comment
        push @cf_exports_list, $enabled;     # Enabled
        push @cf_exports_list, $browseable;  # Browseable
        push @cf_exports_list, $public;      # Public
        push @cf_exports_list, $writeable;   # Writeable
      }

      $path = ""; $comment = ""; $enabled = 1; $public = 0; $browseable = 0;
      $writeable = 0; $printable = 0;

      ($section) = (join(' ', @line) =~ /^\[(.*)\]/);
    }
    elsif ($line[0] eq "workgroup" && not &xst_ignore_line($line[1]))
    {
      $cf_workgroup = $line[1];
    }
    elsif ($line[0] eq "path" && not &xst_ignore_line($line[1]))
    {
      $path = $line[1];
    }
    elsif ($line[0] eq "comment" && not &xst_ignore_line($line[1]))
    {
      shift @line;
      $comment = join(' ', @line);
    }
    elsif ($line[0] eq "available" && not &xst_ignore_line($line[1]))
    {
      $enabled = &xst_read_boolean($line[1]);
    }
    elsif ($line[0] =~ /^browse?able/ && not &xst_ignore_line($line[1]))
    {
      $browseable = &xst_read_boolean($line[1]);
    }
    elsif ($line[0] eq "public" && not &xst_ignore_line($line[1]))
    {
      $public = &xst_read_boolean($line[1]);
    }
    elsif ($line[0] eq "guest" && $line[1] eq "ok" &&
           not &xst_ignore_line($line[2]))
    {
      $public = &xst_read_boolean($line[2]);
    }
    elsif ($line[0] =~ /write?able/ && not &xst_ignore_line($line[1]))
    {
      $writeable = &xst_read_boolean($line[1]);
    }
    elsif ($line[0] eq "printable" && not &xst_ignore_line($line[1]))
    {
      $printable = &xst_read_boolean($line[1]);
    }
  }

  # Push the last share found, if any.

  if (($path ne "") && !$printable)
  {
    push @cf_exports_list, "smb";        # Type
    push @cf_exports_list, $path;        # Path
    push @cf_exports_list, $section;     # Name
    push @cf_exports_list, $comment;     # Comment
    push @cf_exports_list, $enabled;     # Enabled
    push @cf_exports_list, $browseable;  # Browseable
    push @cf_exports_list, $public;      # Public
    push @cf_exports_list, $writeable;   # Writeable
  }

  close(FILE);
}


sub write_smb_conf
{
  local (*INFILE, *OUTFILE);
  my ($section, $printable, $wrote_security) = ("", 0, 0);
  my @buf = ();

  (*INFILE, *OUTFILE) = &xst_open_filter_write_from_names(@smb_conf_names);
  if (not *OUTFILE) { return; }  # No point if we can't write.

  while (<INFILE>)
  {
    @line = split(/[ \n\r\t=]+/, $_);
    if ($line[0] eq "") { shift(@line); }  # Leading whitespace. He.

    if (join(' ', @line) =~ /^\[.*\]/)
    {
      # New section. If last section was a printable, leave it alone
      # (output it as-is).

      if ($printable || $section =~ /^global/i || $section =~ /^homes/i)
      {
        print OUTFILE join('', @buf);
        $printable = 0;
      }
      
      if ($section =~ /^global/i && !$wrote_security)
      {
        print OUTFILE "   security = share\n";
      }

      ($section) = (join(' ', @line) =~ /^\[(.*)\]/);
      @buf = ();
      push @buf, $_;
    }
    elsif ($line[0] eq "printable" && !&xst_ignore_line($line[1]))
    {
      $printable = &xst_read_boolean($line[1]);
      push @buf, $_;
    }
    elsif ($line[0] eq "security" && !&xst_ignore_line($line[1]) &&
           $section =~ /^global/i)
    {
      push @buf, "   security = share\n";
      $wrote_security = 1;
    }
    else
    {
      push @buf, $_;
    }
  }

  # Print section left in the buffer.

  if ($printable || $section =~ /^global/i || $section =~ /^homes/i)
  {
    print OUTFILE join('', @buf);
  }
  
  if ($section =~ /^global/i && !$wrote_security)
  {
    print OUTFILE "   security = share\n";
  }

  # Now, print our exports at the end.
  
  my @shares = @cf_exports_list;
  
  while (@shares)
  {
    if ($shares[0] ne "")
    {
      if ($shares[0] eq "smb")
      {
        print OUTFILE "\n[$shares[2]]\n",
                      "   path = $shares[1]\n",
                      "   available = ", &xst_print_boolean_yesno($shares[4]), "\n",
                      "   browseable = ", &xst_print_boolean_yesno($shares[5]), "\n",
                      "   writable = ", &xst_print_boolean_yesno($shares[7]), "\n",
                      "   public = ", &xst_print_boolean_yesno($shares[6]), "\n";

        if ($shares[3] ne "") { print OUTFILE "   comment = $shares[3]\n"; }

        shift @shares; shift @shares; shift @shares; shift @shares;
        shift @shares; shift @shares; shift @shares; shift @shares;
      }
      elsif ($shares[0] eq "nfs")
      { shift @shares; shift @shares; shift @shares; }
    }
  }
  
  close(INFILE);
  close(OUTFILE);
}


# /etc/fstab
#
# <device> <mount point> <filesystem> <option,option,...> <dump> <fsck>
# <device> <mount point> smbfs        defaults            0      0
# <device> <mount point> nfs          defaults            0      0
# <device> <mount point> <filesystem> <option,option,...> <dump> <fsck>
#
# Exists: (Presumably everywhere)
#
# Absent: (Presumably nowhere)

sub read_fstab
{
  my $ifh;
  local *FILE;

  # Find the file.

  $ifh = &xst_open_read_from_names(@fstab_names);
  if (not $ifh) { return; }  # We didn't find it.
  *FILE = $ifh;

  # Parse the file.

  while (<FILE>)
  {
    my ($host, $path, $type);

    @line = split(/[ \n\r\t]+/, $_);

    if ($line[0] eq "") { shift @line; }
    if ($line[0] eq "") { next; }
    if (&xst_ignore_line($line[0])) { next; }
    ($device, $dir, $fs, $options) = @line;

    $host = ""; $path = "";
    ($password) = ($options =~ /password=([^ \t,]*)/);
    ($user) = ($user =~ /username=([^ \t,]*)/);
    if ($dir eq "none") { $dir = ""; }

    if ($fs eq "smbfs")
    {
      ($host) = ($device =~ /[\/\\]*([^\/\\]+)/);
      ($path) = ($device =~ /[\/\\]*[^\/\\]+(.*)/);
      $path =~ tr/\\/\//;
      $type = "smb";
    }
    elsif ($fs eq "nfs")
    {
      ($host) = ($device =~ /(.*):/);
      ($path) = ($device =~ /.*:(.*)/);
      $type = "nfs";
    }

    # (Find and update) or (add) our internal mount record.

    if ($host ne "" && $path ne "")
    {
      $found = 0;
      for ($i = 0; $cf_shares_list[$i] ne ""; )
      {
        if (($cf_shares_list[$i] =~ /^$host$/i) == 1 &&
            ($cf_shares_list[$i + 1] =~ /^$path$/i) == 1 &&
            ($cf_shares_list[$i + 5] eq $type))
        {
          # Found. Flag it as mounted and &set extra mount parameters.

          if (!($options =~ /noauto/)) { $cf_shares_list[$i + 3] = 1; }  # Mounted
          $cf_shares_list[$i + 10] = 1;  # Listed
          $cf_shares_list[$i + 6] = $user;
          $cf_shares_list[$i + 7] = $password;
          $cf_shares_list[$i + 8] = $dir;
          $found = 1; last;
        }

        $i = $i + 11;  # Host, path, comment, mounted, detected, type, user,
                       # password, point, printed, listed.
      }
      
      if (!$found)
      {
        push(@cf_shares_list, $host);      # Host
        push(@cf_shares_list, $path);      # Path
        push(@cf_shares_list, "");         # Comment
        if (!($options =~ /noauto/)) { push(@cf_shares_list, 1); }  # Mounted
        else { push(@cf_shares_list, 0); }                          # Mounted
        push(@cf_shares_list, 0);          # Detected
        push(@cf_shares_list, $type);      # Type
        push(@cf_shares_list, $user);      # User
        push(@cf_shares_list, $password);  # Password
        push(@cf_shares_list, $dir);       # Point
        push(@cf_shares_list, 0);          # Printed
        push(@cf_shares_list, 1);          # Listed
      }
    }
  }

  close(FILE);
}


sub write_fstab
{
  my ($ifh, $ofh);
  local (*INFILE, *OUTFILE);
  
  ($ifh, $ofh) = &xst_open_filter_write_from_names(@fstab_names);
  if (not $ofh) { return; }  # No point if we can't write.
  *INFILE = $ifh; *OUTFILE = $ofh;

  while (<INFILE>)
  {
    my ($host, $path, $type);

    @line = split(/[ \n\r\t]+/, $_);

    if ($line[0] eq "") { shift @line; }
    if ($line[0] eq "") { print OUTFILE; next; }
    if (&xst_ignore_line($line[0])) { print OUTFILE; next; }
    ($device, $dir, $fs, $options) = @line;

    $host = ""; $path = "";
    ($password) = ($options =~ /password=([^ \t,]*)/);
    ($user) = ($user =~ /username=([^ \t,]*)/);

    if ($fs eq "smbfs")
    {
      ($host) = ($device =~ /[\/\\]*([^\/\\]+)/);
      ($path) = ($device =~ /[\/\\]*[^\/\\]+(.*)/);
      $path =~ tr/\\/\//;
      $type = "smb";
    }
    elsif ($fs eq "nfs")
    {
      ($host) = ($device =~ /(.*):/);
      ($path) = ($device =~ /.*:(.*)/);
      $type = "nfs";
    }
    else
    {
      print OUTFILE; next;
    }

    # Try to find a corresponding internal mount record and insert it here.

    if ($host ne "" && $path ne "")
    {
      $found = 0;
    
      for ($i = 0; $cf_shares_list[$i] ne ""; )
      {
        if (($cf_shares_list[$i] =~ /^$host$/i) == 1 &&
            ($cf_shares_list[$i + 1] =~ /^$path$/i) == 1 &&
            ($cf_shares_list[$i + 5] eq $type))
        {
          # Found. Write it out.
          
          if (!($cf_shares_list[$i + 10]))
          {
            # Stay unlisted.
          }
          elsif ($type eq "smb")
          {
            # FIXME: The fmask here needs to be user-controllable through
            # a multiple-choice interface. It's too permissive right now.

            print OUTFILE "//$cf_shares_list[$i]$cf_shares_list[$i + 1] ";

            if ($cf_shares_list[$i + 8] ne "") { print OUTFILE "$cf_shares_list[$i + 8]"; }
            else { print OUTFILE "none"; }

            print OUTFILE " smbfs username=$cf_shares_list[$i + 6],",
                          "password=$cf_shares_list[$i + 7],fmask=0777";
            if (!($cf_shares_list[$i + 3])) { print OUTFILE ",noauto"; }

            # Options merging and printing. Note that we always write some
            # options, above.

            my @options = ($options =~ /([a-zA-Z0-9=]+),?/mg);
            for $option (@options)
            {
              # Strip options we handle, keep the rest.

              if ($option eq "auto" || $option eq "noauto" ||
                  $option eq "defaults" || $option =~ /^username/ ||
                  $option =~ "^password" || $option =~ /^fmask/) { next; }

              print $OUTFILE "," . $option;
            }

            # Dump and fsck priority.

            print OUTFILE " 0 0\n";
          }
          elsif ($type eq "nfs")
          {
            print OUTFILE "$cf_shares_list[$i]:$cf_shares_list[$i + 1] ";

            if ($cf_shares_list[$i + 8] ne "") { print OUTFILE "$cf_shares_list[$i + 8]"; }
            else { print OUTFILE "none"; }
            
            print OUTFILE " nfs ";

            # Options merging and printing.

            my $prev = 0;

            if (!($cf_shares_list[$i + 3]))
            {
              print OUTFILE "noauto"; $prev = 1;
            }

            my @options = ($options =~ /([a-zA-Z0-9=]+),?/mg);
            for $option (@options)
            {
              # Strip options we handle, keep the rest.

              if ($option eq "auto" || $option eq "noauto" ||
                  $option eq "defaults") { next; }
              if ($prev) { print OUTFILE ","; }
              print OUTFILE $option;
              $prev = 1;
            }

            if (!$prev) { print OUTFILE "defaults"; }

            # Dump and fsck priority.

            print OUTFILE " 0 0\n";
          }
          else
          {
            # Unsupported share type. This is actually caught earlier.
          }

          $found = 1;
          $cf_shares_list[$i + 9] = 1;  # Printed
        }

        $i = $i + 11;  # Host, path, comment, mounted, detected, type, user,
                       # password, point, printed, listed.
      }

      if (!$found) { print OUTFILE; }
    }
  }

  # Print leftover shares.

  for ($i = 0; $cf_shares_list[$i] ne ""; )
  {
    if (!($cf_shares_list[$i + 9]))
    {
      # Not already printed. Do it now.

      if (!($cf_shares_list[$i + 10]))
      {
        # Stay unlisted.
      }
      elsif ($cf_shares_list[$i + 5] eq "smb")
      {
        # FIXME: The fmask here needs to be user-controllable through
        # a multiple-choice interface. It's too permissive right now.

        print OUTFILE "//$cf_shares_list[$i]$cf_shares_list[$i + 1] ";

        if ($cf_shares_list[$i + 8] ne "") { print OUTFILE "$cf_shares_list[$i + 8]"; }
        else { print OUTFILE "none"; }

        print OUTFILE " smbfs ",
                      "username=$cf_shares_list[$i + 6],",
                      "password=$cf_shares_list[$i + 7],fmask=0777";
        if (!($cf_shares_list[$i + 3])) { print OUTFILE ",noauto"; }
        print OUTFILE " 0 0\n";
      }
      elsif ($cf_shares_list[$i + 5] eq "nfs")
      {
        print OUTFILE "$cf_shares_list[$i]:$cf_shares_list[$i + 1] ";

        if ($cf_shares_list[$i + 8] ne "") { print OUTFILE "$cf_shares_list[$i + 8]"; }
        else { print OUTFILE "none"; }

        print OUTFILE " nfs ";

        if (!($cf_shares_list[$i + 3])) { print OUTFILE "noauto"; }
        else { print OUTFILE "defaults"; }
        print OUTFILE " 0 0\n";
      }
      else
      {
        # Unsupported share type. This is actually caught earlier.
      }

      $cf_shares_list[$i + 9] = 1;  # Printed
    }

    $i = $i + 11;  # Host, path, comment, mounted, detected, type, user,
                   # password, point, printed, listed.
  }

  close(INFILE);
  close(OUTFILE);
}


# /etc/exports
#
# <path> <client spec>(<option>,<option>,...) <client spec>(...) ...
# <path> <client spec>(<option>,<option>,...) <client spec>(...) ...
# ...
#
# Exists: (Presumably everywhere)
#
# Absent: (Presumably nowhere)

sub read_exports
{
  my $ifh;
  local *FILE;

  # Find the file.

  $ifh = &xst_open_read_from_names(@exports_names);
  if (not $ifh) { return; }  # We didn't find it.
  *FILE = $ifh;

  # Parse the file.

  while (<FILE>)
  {
    my $host;
    my $clients = [];

    @line = split(/[ \n\r\t]+/, $_);

    if ($line[0] eq "") { shift @line; }
    if ($line[0] eq "") { next; }
    if (&xst_ignore_line($line[0])) { next; }

    $path = $line[0]; shift @line;
    push @cf_exports_list, "nfs";  # Type
    push @cf_exports_list, $path;  # Path

    for $client (@line)
    {
      ($spec, $options) = ($client =~ /^([^\(]+)\(([^\)]+)/);
      push @$clients, $spec;                        # Spec
      if ($options =~ /rw/) { push @$clients, 1; }  # Writeable
      else                  { push @$clients, 0; }  # Writeable
    }

    push @cf_exports_list, $clients;  # Client patterns and options
  }

  close(FILE);
}


sub write_exports
{
  my $ofh;
  local *FILE;

  # Find the file.

  $ofh = &xst_open_write_from_names(@exports_names);
  if (not $ofh) { return; }
  *FILE = $ofh;

  # We rewrite everything, as we have complete command of the things to go
  # in this file.

  my @shares = @cf_exports_list;

  while (@shares)
  {
    if ($shares[0] ne "")
    {
      if ($shares[0] eq "smb")
      {
        shift @shares; shift @shares; shift @shares; shift @shares;
        shift @shares; shift @shares; shift @shares; shift @shares;
      }
      elsif ($shares[0] eq "nfs")
      {
        print FILE "$shares[1]";  # Path

        my $clients = $shares[2];
        while (@$clients)
        {
          if ($$clients[0] ne "")
          {
            print FILE " $$clients[0]";               # Pattern
            if ($$clients[1]) { print FILE "(rw)"; }  # Writeable
          }
          shift @$clients; shift @$clients;
        }

        print FILE "\n";
        shift @shares; shift @shares; shift @shares;
      }
    }
  }
  
  close(FILE);
}


# --- XML parsing --- #


# Scan XML from standard input to an internal tree.

sub xml_parse
{
  # Scan XML to tree.

  $tree = &xst_xml_scan;

  # Walk the tree recursively and extract configuration parameters.
  # This is the top level - find and enter the toplevel tag.

  while (@$tree)
  {
    if ($$tree[0] eq "shares") { &xml_parse_toplevel($$tree[1]); }

    shift @$tree;
    shift @$tree;
  }

  return($tree);
}


sub xml_parse_toplevel
{
  my $tree = $_[0];

  shift @$tree;  # Skip attributes.

  while (@$tree)
  {
    if ($$tree[0] eq "import") { &xml_parse_import($$tree[1]); }
    elsif ($$tree[0] eq "export") { &xml_parse_export($$tree[1]); }

    shift @$tree;
    shift @$tree;
  }
}

sub xml_parse_import
{
  my $tree = $_[0];
  my $type = "", $host = "", $path = "", $comment = "", $user = "",
     $password = "", $mounted = 0, $detected = 0, $point = "", $listed = 0;

  $type = $$tree[0]->{type};
  shift @$tree;
  
  while (@$tree)
  {
    if ($$tree[0] eq "host")        { $host = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "path")     { $path = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "user")     { $user = &xst_xml_get_text($$tree[1]); }
    elsif ($$tree[0] eq "password") { $password = &xst_xml_get_text($$tree[1]); }
    elsif ($$tree[0] eq "comment")  { $comment = &xst_xml_get_text($$tree[1]); }
    elsif ($$tree[0] eq "point")    { $point = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "mounted")  { $mounted = &xml_parse_mounted($$tree[1]); }
    elsif ($$tree[0] eq "detected") { $detected = &xml_parse_detected($$tree[1]); }
    elsif ($$tree[0] eq "listed")   { $listed = &xml_parse_listed($$tree[1]); }

    shift @$tree;
    shift @$tree;
  }

  push(@cf_shares_list, $host);      # Host
  push(@cf_shares_list, $path);      # Path
  push(@cf_shares_list, $comment);   # Comment
  push(@cf_shares_list, $mounted);   # Mounted
  push(@cf_shares_list, $detected);  # Detected
  push(@cf_shares_list, $type);      # Type
  push(@cf_shares_list, $user);      # User
  push(@cf_shares_list, $password);  # Password
  push(@cf_shares_list, $point);     # Point
  push(@cf_shares_list, 0);          # Printed
  push(@cf_shares_list, $listed);    # Listed
}

sub xml_parse_mounted
{
  my $tree = $_[0];

  # Check attribute; 'yes', 'true', 'no', 'false'.

  return(&xst_read_boolean($$tree[0]->{state}));
}


sub xml_parse_detected
{
  my $tree = $_[0];

  # Check attribute; 'yes', 'true', 'no', 'false'.

  return(&xst_read_boolean($$tree[0]->{state}));
}


sub xml_parse_listed
{
  my $tree = $_[0];

  # Check attribute; 'yes', 'true', 'no', 'false'.

  return(&xst_read_boolean($$tree[0]->{state}));
}


sub xml_parse_export
{
  my $tree = $_[0];
  my $type = "";

  $type = $$tree[0]->{type};
  shift @$tree;
  
  if ($type eq "nfs")
  {
    my $path = "", $clients = [];
    
    while (@$tree)
    {
      if ($$tree[0] eq "path")          { $path = &xst_xml_get_word($$tree[1]); }
      elsif ($$tree[0] eq "allow")      { push(@$clients, &xml_parse_allow($$tree[1])); }

      shift @$tree;
      shift @$tree;
    }
    
    if ($path ne "")
    {
      push @cf_exports_list, "nfs";     # Type
      push @cf_exports_list, $path;     # Path
      push @cf_exports_list, $clients;  # Clients
    }
  }
  elsif ($type eq "smb")
  {
    my $name = "", $path = "", $comment = "", $enabled = 0, $browseable = 0,
       $public = 0, $writeable = 0;

    while (@$tree)
    {
      if ($$tree[0] eq "name")          { $name = &xst_xml_get_word($$tree[1]); }
      elsif ($$tree[0] eq "path")       { $path = &xst_xml_get_word($$tree[1]); }
      elsif ($$tree[0] eq "comment")    { $comment = &xst_xml_get_text($$tree[1]); }
      elsif ($$tree[0] eq "enabled")    { $enabled = &xml_parse_enabled($$tree[1]); }
      elsif ($$tree[0] eq "browse")     { $browseable = &xml_parse_browseable($$tree[1]); }
      elsif ($$tree[0] eq "public")     { $public = &xml_parse_public($$tree[1]); }
      elsif ($$tree[0] eq "write")      { $writeable = &xml_parse_writeable($$tree[1]); }

      shift @$tree;
      shift @$tree;
    }

    if ($path ne "")
    {
      push @cf_exports_list, "smb";        # Type
      push @cf_exports_list, $path;        # Path
      push @cf_exports_list, $name;        # Name
      push @cf_exports_list, $comment;     # Comment
      push @cf_exports_list, $enabled;     # Enabled
      push @cf_exports_list, $browseable;  # Browseable
      push @cf_exports_list, $public;      # Public
      push @cf_exports_list, $writeable;   # Writeable
    }
  }
  else
  {
    # Unsupported share type.
    
    return;
  }
}


sub xml_parse_allow
{
  my $tree = $_[0];
  my $pattern = "", $write = 0;
  my @list;

  shift @$tree;  # No attributes.

  while (@$tree)
  {
    if ($$tree[0] eq "pattern")       { $pattern = &xst_xml_get_word($$tree[1]); }
    elsif ($$tree[0] eq "write")      { $write = &xml_parse_writeable($$tree[1]); }

    shift @$tree;
    shift @$tree;
  }

  push @list, $pattern;
  push @list, $write;
  return @list;
}


sub xml_parse_enabled
{
  my $tree = $_[0];

  # Check attribute; 'yes', 'true', 'no', 'false'.

  return(&xst_read_boolean($$tree[0]->{state}));
}


sub xml_parse_browseable
{
  my $tree = $_[0];

  # Check attribute; 'yes', 'true', 'no', 'false'.

  return(&xst_read_boolean($$tree[0]->{state}));
}


sub xml_parse_public
{
  my $tree = $_[0];

  # Check attribute; 'yes', 'true', 'no', 'false'.

  return(&xst_read_boolean($$tree[0]->{state}));
}


sub xml_parse_writeable
{
  my $tree = $_[0];

  # Check attribute; 'yes', 'true', 'no', 'false'.

  return(&xst_read_boolean($$tree[0]->{state}));
}


# --- XML printing --- #


sub xml_print
{
  print "<?xml version='1.0' encoding='ISO-8859-1' standalone='yes'?>\n";
  print "<!DOCTYPE shares []>\n\n";
  print "<shares>\n";
  &xst_xml_enter ();

  &xst_xml_vspace ();
  &xst_xml_print ("<!-- Configuration starts here -->\n");
  &xst_xml_vspace ();
  &xst_xml_print ("<!-- Imports, listed and potentials -->\n");
  &xst_xml_vspace ();

  my @shares = @cf_shares_list;

  while (@shares)
  {
    if ($shares[0] ne "")
    {
      &xst_xml_vspace ();
      &xst_xml_print ("<import type='$shares[5]'>\n");
      &xst_xml_enter ();

      $shares[0] = &xst_xml_quote($shares[0]);
      $shares[1] = &xst_xml_quote($shares[1]);
      $shares[2] = &xst_xml_quote($shares[2]);
      $shares[6] = &xst_xml_quote($shares[6]);
      $shares[7] = &xst_xml_quote($shares[7]);

      &xst_xml_print ("<host>$shares[0]</host>\n");
      &xst_xml_print ("<path>$shares[1]</path>\n");
      if ($shares[6] ne "") { &xst_xml_print ("<user>$shares[6]</user>\n"); }
      if ($shares[7] ne "") { &xst_xml_print ("<password>$shares[7]</password>\n"); }
      if ($shares[2] ne "") { &xst_xml_print ("<comment>$shares[2]</comment>\n"); }
      if ($shares[8] ne "") { &xst_xml_print ("<point>$shares[8]</point>\n"); }

      &xst_xml_indent ();

      print "<mounted state='";
      print &xst_print_boolean_truefalse($shares[3]);
      print "'/>";

      print "<listed state='";
      print &xst_print_boolean_truefalse($shares[10]);
      print "'/>";

      print "<detected state='";
      print &xst_print_boolean_truefalse($shares[4]);
      print "'/>\n";

      &xst_xml_leave ();
      &xst_xml_print ("</import>\n");
    }
    
    shift @shares; shift @shares; shift @shares; shift @shares; shift @shares;
    shift @shares; shift @shares; shift @shares; shift @shares; shift @shares;
    shift @shares;
  }

  &xst_xml_vspace ();
  &xst_xml_print ("<!-- Exports -->\n");
  &xst_xml_vspace ();
  
  my @shares = @cf_exports_list;
  
  while (@shares)
  {
    if ($shares[0] ne "")
    {
      &xst_xml_vspace ();
      &xst_xml_print ("<export type='$shares[0]'>\n");
      &xst_xml_enter ();

      &xst_xml_print ("<path>$shares[1]</path>\n");

      if ($shares[0] eq "smb")
      {
        &xst_xml_print ("<name>$shares[2]</name>\n");
        if ($shares[3] ne "") { &xst_xml_print ("<comment>$shares[3]</comment>\n"); }

        &xst_xml_indent ();
        print "<enabled state='", &xst_print_boolean_truefalse($shares[4]), "'/>";
        print "<browse state='", &xst_print_boolean_truefalse($shares[5]), "'/>";
        print "<public state='", &xst_print_boolean_truefalse($shares[6]), "'/>\n";
        &xst_xml_indent ();
        print "<write state='", &xst_print_boolean_truefalse($shares[7]), "'/>\n";

        shift @shares; shift @shares; shift @shares; shift @shares;
        shift @shares; shift @shares; shift @shares; shift @shares;
      }
      elsif ($shares[0] eq "nfs")
      {
        my $clients = $shares[2];
        while (@$clients)
        {
          if ($$clients[0] ne "")
          {
            &xst_xml_print ("<allow><pattern>$$clients[0]</pattern><write state='");
            print &xst_print_boolean_truefalse($$clients[1]);
            print "'/></allow>\n";
          }
          shift @$clients; shift @$clients;
        }

        shift @shares; shift @shares; shift @shares;
      }

      &xst_xml_leave ();
      &xst_xml_print ("</export>\n");
    }
    
  }
  
  &xst_xml_vspace ();
  &xst_xml_print ("<!-- End of configuration -->\n");
  &xst_xml_vspace ();

  &xst_xml_leave ();
  print "</shares>\n";
}


# --- IP calculation --- #

# Both functions take two arguments: IP and netmask.


sub ip_calc_network
{
  my @ip_reg1;
  my @ip_reg2;

  @ip_reg1 = ($_[0] =~ /([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/);
  @ip_reg2 = ($_[1] =~ /([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/);
  
  $ip_reg1[0] = ($ip_reg1[0] * 1) & ($ip_reg2[0] * 1);
  $ip_reg1[1] = ($ip_reg1[1] * 1) & ($ip_reg2[1] * 1);
  $ip_reg1[2] = ($ip_reg1[2] * 1) & ($ip_reg2[2] * 1);
  $ip_reg1[3] = ($ip_reg1[3] * 1) & ($ip_reg2[3] * 1);
  
  return(join('.', @ip_reg1));
}


sub ip_calc_broadcast
{
  my @ip_reg1;
  my @ip_reg2;

  @ip_reg1 = ($_[0] =~ /([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/);
  @ip_reg2 = ($_[1] =~ /([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/);
  
  @ip_reg1 = ($cf_hostip =~ /([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/);

  $ip_reg1[0] = ($ip_reg1[0] * 1) | (~($ip_reg2[0] * 1) & 255);
  $ip_reg1[1] = ($ip_reg1[1] * 1) | (~($ip_reg2[1] * 1) & 255);
  $ip_reg1[2] = ($ip_reg1[2] * 1) | (~($ip_reg2[2] * 1) & 255);
  $ip_reg1[3] = ($ip_reg1[3] * 1) | (~($ip_reg2[3] * 1) & 255);

  return(join('.', @ip_reg1));
}


# --- Get (read) config --- #


sub get_interfaces
{
  my $if_cmd;
  my $iftext;
  my @list;

  # Get a list of all interfaces, both configured and unconfigured.
  # Should work on both GNU and Solaris.

  $if_cmd = &xst_locate_tool("ifconfig");
  $iftext = `$if_cmd -a 2>/dev/null`;

  @list = ($iftext =~ /^([a-z0-9]+)[ :\t].*/mg);

  if (not @list)
  {
    &xst_report_warning(01, "Could not find any network devices");
    return(0);
  }
  
  # Remove loopback and dummy interfaces from the list.
  
  for $elem (@list)
  {
    if (!($elem =~ /^(lo)|(dummy).*/)) { push(@cf_interface_list, $elem); }
  }
}


sub get_host_mounts
{
  my ($ping_tool, $showmount_tool, $nmblookup_tool, $smbclient_tool,
      $broadcast_ip, $network_ip);
  my @hosts;
  my ($i, $j);

  $ping_tool = &xst_locate_tool("ping");
  $showmount_tool = &xst_locate_tool("showmount");
  $nmblookup_tool = &xst_locate_tool("nmblookup");
  $smbclient_tool = &xst_locate_tool("smbclient");

  $broadcast_ip = &ip_calc_broadcast($cf_hostip, $cf_netmask);
  $network_ip = &ip_calc_network($cf_hostip, $cf_netmask);

  &xst_report_info(15, "Mapping IP subnet " . $network_ip);

  # Map subnet.

  open(PING_HD, "$ping_tool -b -c 2 -n $broadcast_ip 2>/dev/null |");
  while (<PING_HD>)
  {
    if (/bytes from/)
    {
      $ip = join('.', ($_ =~ /from ([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/));
      &xst_push_unique(\@hosts, $ip);
    }
  }
  close(PING_HD);

  if ($#hosts == -1)
  {
    # If nothing was found, make a second attempt, this time with fewer
    # options. Eliminate those that ping might not understand.

    open(PING_HD, "$ping_tool -c 2 -n $broadcast_ip 2>/dev/null |");
    while (<PING_HD>)
    {
      if (/bytes from/)
      {
        $ip = join('.', ($_ =~ /from ([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/));
        &xst_push_unique(\@hosts, $ip);
      }
    }
    close(PING_HD);
  }

  &xst_report_info(1, "Subnet ping found " . ($#hosts + 1) . " hosts");
  &xst_print_progress ();

  # List NFS shares.

  if ($showmount_tool eq "")
  {
    &xst_report_error(1, "Can't locate showmount - NFS scan skipped");
  }
  else
  {
    &xst_report_info(16, "Doing NFS detection on broadcast " . $broadcast_ip);
  
    $i = 0; $j = 0;
    for $host (@hosts)
    {
      if ($host eq $cf_hostip) { next; }
      &xst_report_info(17, "Querying NFS service on " . $host);
      open(MOUNTS_HD, "$showmount_tool --directories --no-headers $host 2>/dev/null |");
      while (<MOUNTS_HD>)
      {
        if (/[Dd]irectories.*:/) { next; }
        push(@cf_shares_list, $host);                          # Host
        push(@cf_shares_list, ($_ =~ /[ \t]*([^ \t\n\r]+)/));  # Path
        push(@cf_shares_list, "");                             # Comment
        push(@cf_shares_list, 0);                              # Mounted
        push(@cf_shares_list, 1);                              # Detected
        push(@cf_shares_list, "nfs");                          # Type
        push(@cf_shares_list, "");                             # User
        push(@cf_shares_list, "");                             # Password
        push(@cf_shares_list, "");                             # Mount point
        push(@cf_shares_list, 0);                              # Flag: Printed.
        push(@cf_shares_list, 0);                              # Listed.
      
        $j++;
      }
      close(MOUNTS_HD);

      $i++;
    }

    &xst_report_info(02, "Found $j NFS exports on $i hosts");
    &xst_print_progress ();
  }

  # List SMB shares if applicable.

  if ($cf_workgroup ne "")
  {
    my ($smb_master_ip, $smb_master_name);
    my @smb_hosts;
    
    &xst_report_info(03, "Doing SMB detection in workgroup $cf_workgroup");

    # Find the workgroup master.
  
    open(FD, "$nmblookup_tool -M -S $cf_workgroup |");
    while (<FD>)
    {
      if (/^[a-zA-Z0-9.]+ $cf_workgroup</)
      {
        ($smb_master_ip) = ($_ =~ /(^[a-zA-Z0-9.]+)/);
      }
      elsif (/^[ \t]+[A-Za-z0-9]+[ \t]+</)
      {
        ($smb_master_name) = ($_ =~ /^[ \t]+([A-Za-z0-9]+)/);
        last;
      }
    }
    close(FD);

    &xst_print_progress ();

    if ($smb_master_name ne "")
    {
      &xst_report_info(04, "Workgroup master is $smb_master_name ($smb_master_ip)");

      # List computers.

      open(FD, "$smbclient_tool -N -U % -L $smb_master_name -I $smb_master_ip |");
      while (<FD>) { if (/^[ \t]+Server[ \t]+Comment/) { last; } }

      while (<FD>)
      {
        if ($_ eq "" || $_ eq "\n") { last; }
        elsif (/^[ \t]+[^ \t-]+/)
        {
          &xst_push_unique(\@smb_hosts, ($_ =~ /^[ \t]+([^ \t-]+)/));
        }
      }
      close(FD);

      if ($#smb_hosts + 1) { &xst_report_info(05, "Master reported " . ($#smb_hosts + 1) . " hosts in workgroup"); }
      else { &xst_report_info(06, "No hosts reported by master - asking others"); }
    }
    else { &xst_report_warning(02, "No workgroup master found"); }

    if (!@smb_hosts)
    {
      # The master browser didn't list any hosts for some reason.
      # This happens fairly often (but randomly) on my network, so we work
      # around it by asking some other hosts to make sure.

      for $host (@hosts)
      {
        if ($host eq $smb_master_ip) { next; }  # Already asked the fucker.

        # Typically only Unix machines (samba) will understand these requests,
        # since they have service entries for their own IPs, whereas Windows
        # boxen don't.
        
        &xst_report_info(07, "Asking $host for host list");

        open(FD, "$smbclient_tool -N -U % -L $host -I $host |");
        while (<FD>) { if (/^[ \t]+Server[ \t]+Comment/) { last; } }
        
        while (<FD>)
        {
          if ($_ eq "" || $_ eq "\n") { last; }
          elsif (/^[ \t]+[^ \t-]+/)
          {
            &xst_push_unique(\@smb_hosts, ($_ =~ /^[ \t]+([^ \t-]+)/));
          }
        }
        close(FD);

        if (@smb_hosts)
        {
          &xst_report_info(8, "Got " . ($#smb_hosts + 1) . " SMB hosts");
          last;
        }
        else { &xst_report_warning(9, "No SMB hosts found"); }
      }
    }

    # List shares on each computer.

    $i = 0; $j = 0;
    while (@smb_hosts)
    {
      open(FD, "$smbclient_tool -N -U % -L $smb_hosts[0] |");
      while (<FD>) { if (/^[ \t]+Sharename[ \t]+Type[ \t]+Comment/) { last; } }
      while (<FD>)
      {
        if ($_ eq "" || $_ eq "\n") { last; }
        elsif (/^[ \t]+[^ \t-]+[ \t]+Disk/)
        {
          ($path) = ($_ =~ /^[ \t]+([^ \t-]+)/);  # Path
          $path = "/\L$path";
          $smb_hosts[0] = "\L$smb_hosts[0]";

          push(@cf_shares_list, $smb_hosts[0]);                # Host
          push(@cf_shares_list, $path);                        # Path
          push(@cf_shares_list, ($_ =~ /^[ \t]+[^ \t-]+[ \t]+Disk[ \t]+(.*)$/));  # Comment
          push(@cf_shares_list, 0);                            # Mounted
          push(@cf_shares_list, 1);                            # Detected
          push(@cf_shares_list, "smb");                        # Type
          push(@cf_shares_list, "");                           # User
          push(@cf_shares_list, "");                           # Password
          push(@cf_shares_list, "");                           # Mount point
          push(@cf_shares_list, 0);                            # Flag: Printed
          push(@cf_shares_list, 0);                            # Listed
          
          $j++;
        }
      }
      close(FD);

      shift @smb_hosts;
      $i++;
    }
    
    &xst_report_info(10, "Found $j SMB exports on $i hosts");
    &xst_print_progress ();
  }
  else { &xst_report_warning(10, "No workgroup configured - SMB scan skipped"); }
}


sub get
{
  &xst_report_info(20, "Finding interfaces");

  &get_interfaces; &xst_print_progress ();          # Needed for primary interface detection.
  &read_rc_config;                          # Ditto.
  &read_network_interfaces;                 # Ditto
  &read_sysconfig_network;                  # Ditto.
  &read_primary_interface;                  # IP subnet information.
  &read_smb_conf; &xst_print_progress ();           # SMB workgroup.
#  if ($xst_do_immediate) { &get_host_mounts; }
  &xst_print_progress ();
  &read_fstab;
  &read_exports; &xst_print_progress ();

  &xst_end();
  &xml_print ();
}


# --- Set (write) config --- #


sub set_immediate
{
  my $exportfs_tool;
  my $mount_tool;
  my $umount_tool;

  $exportfs_tool = &xst_locate_tool("exportfs");
  $mount_tool = &xst_locate_tool("mount");
  $umount_tool = &xst_locate_tool("umount");

  # Update exports.

  &xst_service_restart(80, "-D", "samba", "smb", "smbd");
  &xst_progress(60);

  if ($exportfs_tool eq "")
  {
    &xst_report_error(4, "Could not reload NFS export tables");
  }
  else
  {
    system "$exportfs_tool -a >/dev/null 2>/dev/null";
    &xst_report_info(12, "NFS export tables reloaded");
  }

  &xst_progress(80);

  # Update imports.

  my $num_done = 0;

  if (($mount_tool ne "") && ($umount_tool ne ""))
  {
    my @shares = @cf_shares_list;

    while (@shares)
    {
      if ($shares[0] ne "" && $shares[8] ne "")
      {
        if ($shares[3])
        {
          &xst_report_info(13, "Mounting $shares[8]");
          system "$mount_tool $shares[8] >/dev/null 2>/dev/null";
        }
        else
        {
          &xst_report_info(14, "Unmounting $shares[8]");
          system "$umount_tool $shares[8] >/dev/null 2>/dev/null";
        }
      }

      shift @shares; shift @shares; shift @shares; shift @shares; shift @shares;
      shift @shares; shift @shares; shift @shares; shift @shares; shift @shares;
      shift @shares;
      
      $num_done++;
      &xst_progress(80 + (20 / ((($#cf_shares_list + 1) / 11) - $num_done + 1)));
    }
  }
  else
  {
    &xst_report_error(6, "Could not find mount tools. No mounting done");
  }
}


sub set
{
  &xml_parse ();

  &write_fstab; &xst_progress(10);
  &write_exports; &xst_progress(20);
  &write_smb_conf; &xst_progress(30);

  if ($xst_do_immediate)
  {
    &set_immediate;
  }
  
  &xst_end();
}


# --- Filter config: XML in, XML out --- #


sub filter
{
  &xml_parse ();
  &xst_end();
  &xml_print ();
}


# --- Main --- #

&xst_init ($name, $version, $description, @ARGV);
&xst_platform_ensure_supported (@platforms);

# Do our thing.

if    ($xst_operation eq "get")    { &get; }
elsif ($xst_operation eq "set")    { &set; }
elsif ($xst_operation eq "filter") { &filter; }
