#!/usr/bin/perl

# drakroam: wireless network roaming GUI
# beta version uses wlandetect as backend
# Austin Acton, 2005
# <austin@mandrake.org>
# Licensed under the GPL

# problems
# - deletes comments in config file
# - expects an ifcfg file for static IP configurations (not uncommon)
# - roaming status fails (no idea why)
#   maybe same reason bash-completion killall can not see wlandetect?

# todo (wlandetect version)
# - make known and available lists have more rows by default (why so small?)
# - refresh status every x seconds
# - find a good way to drop the access point and resume roaming
# - make 'key' column wider by default
# todo (waproamd version)
# - listen to dbus for pings from waproamd; update all on receiving a dbus ping
# - setup static network configurations
# - handle keys (can key file be named after ESSID?)
# - should files be named as MAC or as essid:ESSID ?

use strict;
use lib qw(/usr/lib/libDrakX);

use standalone;
use common;
use run_program;
use detect_devices;
use Glib qw(TRUE FALSE);
use ugtk2 qw(:create :helpers :wrappers);
use Gtk2::SimpleList;
use Socket;

require_root_capability();

unless (detect_devices::has_wireless()) {
    ugtk2::err_dialog(N("Error"), N("You do not have any wireless interface.
Run the \"%s\" assistant from the Mandriva Linux Control Center", N("Set up a new network interface (LAN, ISDN, ADSL, ...)")));
    ugtk2::exit(0) if !$::testing;
}




# global settings
my $route = '/sbin/route -n';
my $IWList = '/sbin/iwlist';
my $IWConfig = '/sbin/iwconfig';
my $IFConfig = '/sbin/ifconfig';
my $IFUp = '/sbin/ifup';
my $IFDown = '/sbin/ifdown';
my $DHClient = '/sbin/dhclient';
my $enable_roaming = member('--roaming', @ARGV); #- not tested yet ...

# initialize variables
my $ScanInterval = 30; # tell deamon to search for new nets every x seconds

my ($KnownList, $AvailableList);
my $device;

my %available_roaming_daemons = (
		       wlandetect => {
				      config_location => '/etc/wlandetect.conf',
				      binary => '/usr/sbin/wlandetect',
				      start_options => sub {
					  my ($interval, undef) = @_;
					  "-d -t $interval";
				      },
				      read_config => sub {
					  my ($config) = @_;
					  each_index {
					      /^#/ || /^\n/ and next; #ignore comments and blank lines
					      if (/^(\S+)\s+(.*)$/) {
						  my ($essid, $mode, $channel, $dhcp, $key);
						  my $command = $2;
						  # setup new network entry
						  $essid = $1;
						  ($mode) = $command =~ /mode\s([^\s;]+)/;
						  ($channel) = $command =~ /channel\s([^\s;]+)/;
						  ($key) = $command =~ /key\s([^\s;]+)/;
						  $dhcp = $command =~ /dhclient/;
						  &AddNet($essid, $mode, $channel, $dhcp, $key);
					      }
					      else { die "Line $::i of configuration file is not parseable.\n" }
					  } cat_($config);
				      },
				      write_config => sub {
					  my ($config) = @_;
					  my @contents = (
							  "#wlandetect configuration file\n",
							  "#format: essid<tab><tab>commands\n",
							  "#use \@DEV\@ for device name\n"
							 );
					  foreach my $row (@{$KnownList->{data}}) { # again, lame
					      my $essid = $row->[0];
					      my $iwc = join(' ', $IWConfig, "essid $essid",
							     if_($row->[1], "mode $row->[1]"),
							     if_($row->[2], "channel $row->[2]"),
							     if_($row->[4], "key $row->[4]"));
					      my $ifc = $row->[3] ? "$IFConfig \@DEV\@ up; $DHClient \@DEV\@" : "$IFUp \@DEV\@";
					      push @contents, "$essid\t\t$iwc; $ifc\n";
					  }
					  output_with_perm($config, 0600, @contents);
				      },
				     },
		       waproamd => {
				      config_location => '/etc/waproamd/keys',
				      binary => '/usr/sbin/waproamd',
				      start_options => sub {
					  my ($interval, $device) = @_;
					  "-i $device -t $interval";
				      },
				      read_config => sub {
					  my ($config) = @_;
					  foreach my $file (all($config)) {
					      my ($essid) = $file =~ /^(.*)\.wep$/ or next;
					      &AddNet($essid, '', '', 1, cat_("$config/$file"));
					  }
				      },
				      write_config => sub {
					  my ($config) = @_;
					  foreach my $row (@{$KnownList->{data}}) { # again, lame
					      my $essid = $row->[0];
					      output_with_perm("$config/$essid.wep", 0600, $row->[4]);
					  }
				      },
				     },
);

my $roaming_daemon = $available_roaming_daemons{wlandetect};

my $ScanEntry = Gtk2::Entry->new;
$ScanEntry->set_width_chars(4);
$ScanEntry->set_text($ScanInterval);

$KnownList = Gtk2::SimpleList->new(
                                      N("ESSID") => "text",
                                      N("Mode") => "text",
                                      N("Channel") => "text",
                                      N("DHCP") => "bool",
                                      N("Key") => "text"
                                     );
$KnownList->get_selection->set_mode('single');
$KnownList->set_reorderable(1);
$KnownList->set_column_editable(1, TRUE); # allow to change mode
$KnownList->set_column_editable(2, TRUE); # allow to change channel
$KnownList->set_column_editable(4, TRUE); # allow to change key

$AvailableList = Gtk2::SimpleList->new(
                                          "ESSID" => "text",
                                          "Type" => "text",
                                          "Encryption" => "text",
                                          "Signal (%)" => "int"
                                         );
$AvailableList->get_selection->set_mode('single');

my $NetLabel = Gtk2::WrappedLabel->new(N("Network:"));
my $IpLabel = Gtk2::WrappedLabel->new(N("IP:"));
my $GwLabel = Gtk2::WrappedLabel->new(N("Gateway:"));
my $ModeLabel = Gtk2::WrappedLabel->new(N("Mode:"));
my $WepLabel = Gtk2::WrappedLabel->new(N("Encryption:"));
my $SignalLabel = Gtk2::WrappedLabel->new(N("Signal:"));

my $net_field   = Gtk2::WrappedLabel->new;
my $mode_field  = Gtk2::WrappedLabel->new;
my $ip_field    = Gtk2::WrappedLabel->new;
my $wep_field   = Gtk2::WrappedLabel->new;
my $gw_field    = Gtk2::WrappedLabel->new;
my $sig_field   = Gtk2::WrappedLabel->new;


my $w = ugtk2->new('Drakroam');
gtkadd(gtkset_border_width($w->{window}, 2),
       gtkpack_(Gtk2::VBox->new,
                if_($enable_roaming,
                    0, gtkadd(gtkset_border_width(Gtk2::Frame->new(N("Roaming")), 2),
                          gtkpack(create_hbox(),
                                  gtkpack(Gtk2::VBox->new,
                                          my $RoamStatus = Gtk2::Label->new(N("Roaming: %s", N("off"))),
                                          gtkpack(create_hbox(),
						  gtksignal_connect(Gtk2::Button->new(N("Start")), clicked => sub { &StartRoam }),
						  gtksignal_connect(Gtk2::Button->new(N("Stop")), clicked => sub { &StopRoam })
                                                 )
                                         ),
                                  gtkpack(Gtk2::VBox->new,
                                          Gtk2::Label->new(N("Scan interval (sec): ")),
                                          gtkpack(Gtk2::HBox->new,
                                                  $ScanEntry,
                                                  gtksignal_connect(Gtk2::Button->new(N("Set")), clicked => sub { &SetInterval })
                                                 )
                                         )
                                 )
                         )),
                1, gtkadd(gtkset_border_width(Gtk2::Frame->new(N("Known Networks (Drag up/down or edit)")), 2),
                          gtkpack_(Gtk2::VBox->new,
                                   1, create_scrolled_window($KnownList),
                                   0, gtkpack(create_hbox(),
					      gtksignal_connect(Gtk2::Button->new(N("Remove")), clicked => sub {
								    my ($selected) = $KnownList->get_selected_indices;
								    &RemoveNet($selected);
								}),
					      gtksignal_connect(Gtk2::Button->new(N("Connect")), clicked => sub {
								    my ($selected) = $KnownList->get_selected_indices;
								    &ConnectNow($selected);
								}),
					      gtksignal_connect(Gtk2::Button->new(N("Save")), clicked => sub { &WriteConfig })
                                             )
                                  )
                         ),
                1, gtkadd(gtkset_border_width(Gtk2::Frame->new(N("Available Networks")), 2),
                          gtkpack_(Gtk2::VBox->new,
                                   1, create_scrolled_window($AvailableList),
                                   0, gtkpack(create_hbox(),
					      gtksignal_connect(Gtk2::Button->new(N("Add")), clicked => sub {
								    my @selected = $AvailableList->get_selected_indices;
								    my ($mode, $channel, $key);
								    my $essid = $AvailableList->{data}["@selected"][0];
								    my $dhcp = 1; # assume dhcp for new networks
								    if ($essid) {
									&AddNet($essid, $mode, $channel, $dhcp, $key);
								    } else {
									print "ESSID is empty, skipping network\n";
								    }
								}),
					      gtksignal_connect(Gtk2::Button->new(N("Rescan")), clicked => sub { &UpdateAvailable })
                                             )
                                  )
                         ),
                0, gtkadd(gtkset_border_width(Gtk2::Frame->new(N("Status")), 2),
                          gtkpack(Gtk2::VBox->new,
                                  create_packtable({ col_spacings => 5, row_spacings => 5, homogenous => 1 },
                                                   [ $NetLabel,  $net_field,  $IpLabel,  $ip_field,  $GwLabel,     $gw_field ],
                                                   [ $ModeLabel, $mode_field, $WepLabel, $wep_field, $SignalLabel, $sig_field ],
                                                  ),
                                  gtkpack(create_hbox(),
                                          gtksignal_connect(Gtk2::Button->new(N("Disconnect")), clicked => sub { &Disconnect }),
                                          gtksignal_connect(Gtk2::Button->new(N("Refresh")), clicked => sub { &UpdateStatus }),
                                          gtksignal_connect(Gtk2::Button->new(N("Close")), clicked => sub { Gtk2->main_quit })
                                         )
                                 )
                         ),
               )
      );

# fill the GUI
&ReadConfig;
&UpdateAll;

sub UpdateAll {
	&UpdateAvailable; #must go first as it defines the device name
	&UpdateStatus;
	&UpdateRoaming;
}

sub isRoamingRunning() {
	my $name = basename($roaming_daemon->{binary});
	any { /\Q$name\E$/ } run_program::get_stdout("ps", "-A");
}

sub UpdateRoaming() {
	my $status = isRoamingRunning() ? N("on") : N("off");
	$RoamStatus->set_text(N("Roaming: %s", $status));
	return FALSE; #- do not update again if launched on timeout
}

sub UpdateStatus() {
	my $CurrentNet = "-";
	my $CurrentIP = "---.---.---.---";
	my $CurrentGW = "---.---.---.---";
	my $CurrentMode = "";
	my $CurrentWEP = "";
	my $CurrentSignal = "-";
	print "Updating\n" if $::testing;
	foreach (run_program::get_stdout($IWConfig, $device)) {
		/ESSID:"(.*?)"/ and $CurrentNet = $1;
		/Mode:(\S*)\s/ and $CurrentMode = $1;
		/key:(\S*)\s/ and $CurrentWEP = $1;
		m!Quality[:=](\S*)/! and $CurrentSignal = $1;
	}
	foreach (run_program::get_stdout($IFConfig, $device)) {
		if (/inet addr:(\S*)\s/) { $CurrentIP = $1 }
	}
	foreach (run_program::get_stdout($route)) {
		#- FIXME: use timeout for DNS resolution, factorize with activefw.pm
		if (/^0.0.0.0\s*(\S*)\s/) { $CurrentGW = gethostbyaddr(inet_aton($1), AF_INET) }
		else { $CurrentGW = "---.---.---.---" }
	}
	$net_field->set_text($CurrentNet);
	$mode_field->set_text($CurrentMode);
	$ip_field->set_text($CurrentIP);
	$wep_field->set_text($CurrentWEP);
	$gw_field->set_text($CurrentGW);
	$sig_field->set_text($CurrentSignal);
}

sub UpdateAvailable() {
	my ($essid, $mode, $wep, $signal);
	print "Running iwlist\n" if $::testing;
	@{$AvailableList->{data}} = ();
	foreach (`$IWList scan 2>/dev/null`) {
		/([^ ]+)([ \t]+)Scan completed :/ and $device = $1;
		/([^ ]+)([ \t]+)No scan results/ and  $device = $1;
		/ESSID:"(.*?)"/ and $essid = $1;
		/Mode:(\S*)/ and $mode = $1;
		m!Quality[:=](\S*)/! and $signal = $1;
		if (/key:(\S*)\s/) {
			$wep = $1;
			print "ESSID: $essid, Mode: $mode, WEP: $wep, Signal: $signal\n" if $::testing;
			push @{$AvailableList->{data}}, [$essid, $mode, $wep, $signal];
		}
	}
}

sub AddNet {
	my ($essid, $mode, $channel, $dhcp, $key) = @_;
	member($essid, map { $_->[0] } @{$KnownList->{data}}) and return;
	print "Adding net $essid\n" if $::testing;
	push @{$KnownList->{data}}, [ $essid, $mode, $channel, $dhcp, $key ];
}

sub RemoveNet {
	my ($selected) = @_;
	my $essid = $KnownList->{data}[$selected][0];
	print "Removing net $essid\n" if $::testing;
	splice @{$KnownList->{data}}, $selected, 1;
}

sub ReadConfig() {
	$_->{read_config}($_->{config_location}) foreach values %available_roaming_daemons;
}

sub WriteConfig() {
	$_->{write_config}($_->{config_location}) foreach values %available_roaming_daemons;
}

sub StartRoam() {
	my $options = $roaming_daemon->{start_options}($ScanInterval, $device);
	my $name = basename($roaming_daemon->{binary});
	system("killall $name; $roaming_daemon->{binary} $options &");
	Glib::Timeout->add(1000, \&UpdateRoaming);
}

sub StopRoam() {
	my $name = basename($roaming_daemon->{binary});
	system("killall $name");
	Glib::Timeout->add(1000, \&UpdateRoaming);
}

sub SetInterval() {
	$ScanInterval = $ScanEntry->get_text;
	if (isRoamingRunning()) {
	    StopRoam();
	    StartRoam();
	}
}

sub ConnectNow {
	my ($row) = @_;
	my @command = "";
	push @command, "$IWConfig $device essid $KnownList->{data}[$row][0] ";
     my %commands = (1 => 'mode', 2 => 'channel', 4 => 'key');
     push @command, map { "$commands{$_} $KnownList->{data}[$row][$_] " } grep { $KnownList->{data}[$row][$_] } keys %commands;
	push @command, "; ";
	if ($KnownList->{data}[$row][3]) {
		push @command, "$IFConfig $device up; $DHClient $device";
	}
	else {
		push @command, "$IFUp $device";
	}
	my $ToBash = join("", @command);
	print "Sending $ToBash\n" if $::testing;
	system($ToBash);
	&UpdateStatus;
}

sub Disconnect {
	print "Dropping $device\n" if $::testing;
	system("$IFDown $device");
	&UpdateStatus;
}

sub Dialog {
    my ($FilePointer) = @_;
    my $content = join('', cat_($FilePointer));
    # dump into a dialog
    my $AboutWindow = Gtk2::Dialog->new(N("Information"), $w->{real_window},
                                        'destroy-with-parent',
                                        N("Ok") => 'none');
    $AboutWindow->vbox->add(create_scrolled_window(Gtk2::Label->new($content)));
    $AboutWindow->signal_connect(response => sub { $_[0]->destroy });
    $AboutWindow->show_all;
}

$w->main;