#!/usr/bin/perl
# Copyright (c) 2004 Gavin Brown. This program is free software, you can use it
# and/or modify it under the same terms as Perl itself.
#
# $Id: gresolver.pl,v 1.12 2006/01/30 20:54:10 gavin Exp $
use English;
use Gtk2 -init;
use Gtk2::GladeXML;
use Gtk2::Helper;
use File::Temp qw(tmpnam);
use POSIX qw(setlocale);
use Net::IPv6Addr;
use strict;

### variables:
my $NAME	= 'GResolver';
my $SNAME	= lc($NAME);
my $PREFIX	= '@PREFIX@';
my $LOCALE_DIR	= (-d $PREFIX ? "$PREFIX/share/locale"	: get_current_dir().'/locale');
my $LOCALE	= (defined($ENV{LC_MESSAGES}) ? $ENV{LC_MESSAGES} : $ENV{LANG});

### do locale stuff here, so that we can have a translated error message below:
if ($OSNAME ne 'MSWin32') {
	require Locale::gettext;
	eval {
		use POSIX;
		setlocale(LC_ALL, $LOCALE);
		bindtextdomain($SNAME, $LOCALE_DIR);
		textdomain($SNAME);
	};

} else {
	eval {
		sub gettext { shift }
	};

}

my $GLADE_FILE	= (-d $PREFIX ? "$PREFIX/share/$SNAME"	: get_current_dir())."/$SNAME.glade";
my $ICON_FILE	= (-d $PREFIX ? "$PREFIX/share/pixmaps"	: get_current_dir())."/$SNAME.png";
my $VERSION	= '0.0.5';
my $RCFILE	= sprintf('%s/.%src', Glib::get_home_dir(), $SNAME);
my $OPTIONS	= load_options();

my @SERVER_HIST	= split(/\|/, $OPTIONS->{server_hist});
my @QUERY_HIST	= split(/\|/, $OPTIONS->{query_hist});

my $glade = Gtk2::GladeXML->new($GLADE_FILE);
$glade->signal_autoconnect_from_package('main');

if ($OSNAME eq 'MSWin32' && !-e $OPTIONS->{dig}) {
	my $filter = Gtk2::FileFilter->new;
	$filter->add_pattern('dig.exe');
	$filter->set_name('dig.exe');
	$glade->get_widget('dig_locator_chooser')->add_filter($filter);
	$glade->get_widget('dig_locator')->set_icon(Gtk2::Gdk::Pixbuf->new_from_file($ICON_FILE));
	$glade->get_widget('dig_locator')->signal_connect('response', \&on_dig_locator_response, $glade);
	$glade->get_widget('dig_locator')->run;
}

my $DIG;
if ($OSNAME eq 'MSWin32') {
	$DIG = $OPTIONS->{dig};

} else {
	chomp($DIG = `which dig 2>/dev/null`);

}

### we need an executable dig, pop up an error dialog if one can't be found:
if (!-x $DIG) {
	my $dialog = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'ok', gettext("Can't find the 'dig' program!"));
	$dialog->signal_connect('response', sub { exit 1 });
	$dialog->signal_connect('delete_event', sub { exit 1 });
	$dialog->run;
}

my @DIG_VERSION	= get_dig_version();
my $DIG_VERSION	= join('.', @DIG_VERSION);
my $RECURS_PARAM= recursive_param();


# these correspond to rows in the type combobox, which are populated by glade.
# They will break if the order of the rows is changed.
my $ANY_ROW_ID	= 4;
my $PTR_ROW_ID	= 6;

my $ABOUT_DIALOG_MARKUP	= <<"END";
<span weight="bold" size="large">$NAME</span>
<span size="small">v$VERSION, using DiG v$DIG_VERSION</span>

Copyright (c) 2004 Gavin Brown. This program is free software, you can use it and/or modify it under the same terms as Perl itself.
END

my $OVERWRITE_DIALOG_MARKUP = sprintf('<span weight="bold" size="large">%s</span>', gettext('A file named "%s" already exists.'));

my $normal	= Gtk2::Gdk::Cursor->new('left_ptr');
my $busy	= Gtk2::Gdk::Cursor->new('watch');

### build the UI:
my $glade = Gtk2::GladeXML->new($GLADE_FILE);
$glade->signal_autoconnect_from_package('main');

### the application icon:
my $ICON = Gtk2::Gdk::Pixbuf->new_from_file($ICON_FILE);

### set up the about dialog:
$glade->get_widget('about_icon')->set_from_pixbuf($ICON);
$glade->get_widget('about_label')->set_markup($ABOUT_DIALOG_MARKUP);
$glade->get_widget('about_dialog')->set_icon($ICON);

### set up the file overwrite dialog:
my $SAVE_TARGET = '';
$glade->get_widget('overwrite_dialog')->set_icon($ICON);
$glade->get_widget('overwrite_dialog')->signal_connect('close',		sub { $glade->get_widget('overwrite_dialog')->hide_all ; return 1 });
$glade->get_widget('overwrite_dialog')->signal_connect('delete_event',	sub { $glade->get_widget('overwrite_dialog')->hide_all ; return 1 });
$glade->get_widget('overwrite_dialog')->signal_connect('response',	sub { $glade->get_widget('overwrite_dialog')->hide_all ; write_output($SAVE_TARGET) if ($_[1] eq 'ok') ; return 1 });

### set up the output treeview:
my @cols = qw(Host TTL Class Type Answer);
my $store = Gtk2::TreeStore->new(qw/Glib::String Glib::String Glib::String Glib::String Glib::String/);
$glade->get_widget('output')->set_model($store);
for (0..4) {
	$glade->get_widget('output')->insert_column_with_attributes(
		$_,
		gettext($cols[$_]),
		Gtk2::CellRendererText->new,
		text => $_,
	);
}
foreach my $col ($glade->get_widget('output')->get_columns) {
	$col->set_resizable(1);
}
$glade->get_widget('output')->signal_connect('button_release_event', sub { context_menu() if ($_[1]->button == 3) });

### setup the raw output textview:
$glade->get_widget('raw_response')->get_buffer->create_tag(
	'monospace',
	font		=> $OPTIONS->{font},
	wrap_mode	=> 'none',
);


$glade->get_widget('query_combo')->disable_activate;
$glade->get_widget('server_combo')->disable_activate;

### apply user settings:
$glade->get_widget('main_window')->set_position('center');
$glade->get_widget('main_window')->set_icon($ICON);

$glade->get_widget('query_combo')->set_popdown_strings('',	@QUERY_HIST);
$glade->get_widget('server_combo')->set_popdown_strings('',	@SERVER_HIST);

$glade->get_widget('recursive_checkbutton')->set_active(	$OPTIONS->{recursive_on} == 1);
$glade->get_widget('additional_checkbutton')->set_active(	$OPTIONS->{additional_on} == 1);
$glade->get_widget('authoritative_checkbutton')->set_active(	$OPTIONS->{authoritative_on} == 1);
$glade->get_widget('trace_checkbutton')->set_active(		$OPTIONS->{trace_on} == 1);
$glade->get_widget('options_expander')->set_expanded(		$OPTIONS->{options_visible} == 1);
$glade->get_widget('notebook')->set_current_page(		$OPTIONS->{current_page});
$glade->get_widget('type_combo')->set_active(			$OPTIONS->{last_type});

$glade->get_widget('query_menuitem')->set_sensitive(undef);
$glade->get_widget('query_button')->set_sensitive(undef);

$glade->get_widget('main_window')->show_all;

Gtk2->main;

exit 0;

### gather user settings and save config:
sub close_program {
	$OPTIONS->{recursive_on}	= ($glade->get_widget('recursive_checkbutton')->get_active	? 1 : 0);
	$OPTIONS->{additional_on}	= ($glade->get_widget('additional_checkbutton')->get_active	? 1 : 0);
	$OPTIONS->{authoritative_on}	= ($glade->get_widget('authoritative_checkbutton')->get_active	? 1 : 0);
	$OPTIONS->{trace_on}		= ($glade->get_widget('trace_checkbutton')->get_active		? 1 : 0);
	$OPTIONS->{options_visible}	= ($glade->get_widget('options_expander')->get_expanded		? 1 : 0);
	$OPTIONS->{current_page}	= $glade->get_widget('notebook')->get_current_page;
	$OPTIONS->{last_type}		= $glade->get_widget('type_combo')->get_active;
	$OPTIONS->{query_hist}		= join('|', uniq(@QUERY_HIST));
	$OPTIONS->{server_hist}		= join('|', uniq(@SERVER_HIST));
	save_options($OPTIONS);
	exit 0;
}

### make a DNS query:
sub make_query {

	### query options:
	my $type		= $glade->get_widget('type_combo')->get_model->get($glade->get_widget('type_combo')->get_active_iter, 0);
	my $query		= $glade->get_widget('query_combo')->entry->get_text;
	my $server		= $glade->get_widget('server_combo')->entry->get_text;
	my $recursive		= $glade->get_widget('recursive_checkbutton')->get_active;
	my $additional		= $glade->get_widget('additional_checkbutton')->get_active;
	my $authoritative	= $glade->get_widget('authoritative_checkbutton')->get_active;
	my $trace		= $glade->get_widget('trace_checkbutton')->get_active;

	return unless ($query ne '');

	unshift(@QUERY_HIST, $query);
	@QUERY_HIST = uniq(@QUERY_HIST);
	unshift(@SERVER_HIST, $server) if ($server ne '');
	@SERVER_HIST = uniq(@SERVER_HIST);

	$glade->get_widget('query_combo')->set_popdown_strings(@QUERY_HIST);
	$glade->get_widget('server_combo')->set_popdown_strings(@SERVER_HIST) if ($server ne '');
	$glade->get_widget('query_button')->set_sensitive(undef);
	$glade->get_widget('menu')->set_sensitive(undef);
	set_status(gettext('Sending query...'));

	### if we're doing a PTR query and the query string is a dotted-quad IP address,
	### then turn it into an in-addr.arpa domain:
	if ($type eq 'PTR') {
		if (is_ipv4addr($query)) {
			$query = join('.', reverse(split(/\./, $query))).'.in-addr.arpa';
			$glade->get_widget('query_combo')->entry->set_text($query);

		} elsif (is_ipv6addr($query)) {
			$glade->get_widget('query_combo')->entry->set_text(lc(Net::IPv6Addr->new($query)->to_string_ip6_int));

		}
	}

	### build the dig query:
	my $cmd = join(' ', (
		"\"$DIG\"",
		'+'.($recursive		== 1 ? '' : 'no').$RECURS_PARAM,
		'+'.($additional	== 1 ? '' : 'no').'additional',
		'+'.($authoritative	== 1 ? '' : 'no').'authority',
		'+'.($trace		== 1 ? '' : 'no').'trace',
		$type,
		$query,
		($server ne '' ? sprintf('@%s', $server) : ''),
	));

	$glade->get_widget('main_window')->window->set_cursor($busy);
	$store->clear;
	$glade->get_widget('raw_response')->get_buffer->set_text('');

	### read dig's STDOUT, pipe STDERR to a file so we can display it
	### if dig exits with an error:
	my $tmpfile = tmpnam();
	if (!open(DIG, "$cmd 2>$tmpfile|")) {
		$glade->get_widget('main_window')->window->set_cursor($normal);
		$glade->get_widget('menu')->set_sensitive(1);
		$glade->get_widget('query_button')->set_sensitive(1);

		my $dialog = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'ok', sprintf(gettex('Cannot pipe from dig: %s'), $!));
		$dialog->signal_connect('response', sub { $dialog->destroy });
		set_status(gettext(''));
		$dialog->run;

	} else {
		my ($tag, $buffer);
		set_status(gettext('Receiving response...'));
		$tag = Gtk2::Helper->add_watch(fileno(DIG), 'in', sub {
			if (eof(DIG)) {
				close(DIG);
				Gtk2::Helper->remove_watch($tag);
				display($buffer);
				$glade->get_widget('main_window')->window->set_cursor($normal);
				$glade->get_widget('menu')->set_sensitive(1);
				$glade->get_widget('query_button')->set_sensitive(1);
				set_status(gettext(''));
				if ($? > 0) {
					if (!open(TMPFILE, $tmpfile)) {
						print STDERR "Error: couldn't open temporary file '$tmpfile' to tell you about an error from dig: $!\n";
						exit 1;

					} else {
						my $err;
						while (<TMPFILE>) {
							$err .= $_;
						}
						close(TMPFILE);

						my $dialog = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'ok', $err);
						$dialog->signal_connect('response', sub { $dialog->destroy });
						$dialog->run;
					}

				} else {
					unlink($tmpfile);

				}
			} else {
				$buffer .= <DIG>;

			}
		});
	}

	return 1;
}

sub display {
	my $buffer = shift;

	my @lines = split(/\n{1}/, $buffer);
	my $parent = undef;

	$glade->get_widget('raw_response')->get_buffer->insert_with_tags_by_name(
		$glade->get_widget('raw_response')->get_buffer->get_start_iter,
		$buffer,
		'monospace',
	);

	foreach my $line (@lines) {
		if ($line =~ /;; ANSWER SECTION/) {
			my $iter = $store->append(undef);
			$store->set($iter, 0, 'Answer:');
			$parent = $iter;

		} elsif ($line =~ /;; ADDITIONAL SECTION/) {
			my $iter = $store->append(undef);
			$store->set($iter, 0, 'Additional:');
			$parent = $iter;

		} elsif ($line =~ /;; AUTHORITY SECTION/) {
			my $iter = $store->append(undef);
			$store->set($iter, 0, 'Authority:');
			$parent = $iter;

		} elsif ($line !~ /^\s*$/ && $line !~ /^\s*;/) {
			my $iter = $store->append($parent);
			my @parts = split(/\s+/, $line, 5);

			for (0..4) {
				$store->set($iter, $_, $parts[$_]);
			}

		}
	}
	$glade->get_widget('output')->expand_all;
	return 1;
}

sub load_options {
	my $hashref = {
		font			=> 'Courier 10',
		recursive_on		=> 1,
		additional_on		=> 1,
		authoritative_on	=> 1,
		trace_on		=> 0,
		options_visible		=> 0,
		current_page		=> 0,
		last_type		=> 0,
	};
	if (open(RCFILE, $RCFILE)) {
		while (<RCFILE>) {
			chomp;
			my ($name, $value) = split(/\s*=\s*/, $_, 2);
			$hashref->{lc($name)} = $value;
		}
		close(RCFILE);
	}
	return $hashref;
}

sub save_options {
	my $hashref = shift;
	if (!open(RCFILE, ">$RCFILE")) {
		print STDERR "Error opening '$RCFILE': $!\n";
		return undef;

	} else {
		foreach my $name (keys(%{$hashref})) {
			printf(RCFILE "%s=%s\n", $name, $hashref->{$name});
		}
		close(RCFILE);
		return 1;
	}
}

sub uniq {
	my @array = @_;
	my @new;
	my %map;
	foreach my $member (@array) {
		$map{$member}++;
	}
	foreach my $member (@array) {
		if ($map{$member} > 0) {
			push(@new, $member);
			$map{$member} = 0;
		}
	}
	return @new;
}

sub show_about_dialog {
	$glade->get_widget('about_dialog')->set_position('center');
	$glade->get_widget('about_dialog')->show_all;
	return 1;
}

sub hide_about_dialog {
	$glade->get_widget('about_dialog')->hide_all;
	return 1;
}

sub changed {
	my $bool = ($glade->get_widget('query_combo')->entry->get_text ne '');
	$glade->get_widget('query_menuitem')->set_sensitive($bool);
	$glade->get_widget('query_button')->set_sensitive($bool);
	return 1;
}

### return an array containing the major, minor and micro version numbers of the dig program:
sub get_dig_version {
	my $version;
	# an un-argumented call to dig returns the root hints from the default server;
	if (!open(DIG, "\"$DIG\"|")) {
		print STDERR "Cannot pipe from '$DIG': $!\n";
		exit 1;

	} else {
		# ignore the first line:
		<DIG>;
		# capture the next line:
		my $line = <DIG>;
		close(DIG);

		if ($line =~ /DiG ([\d\.]+)/) {
			$version = $1;

		} else {
			print STDERR "Error parsing version output from dig, got:\n\t$line\n";
			exit 1;
		}

	}

	return split(/\./, $version, 3);
}

### the +[no]recurs(iv)e parameter changed between 9.2.x and 9.3.x. This subroutine examines the
### dig version and returns a string containing the correct parameter:
sub recursive_param {
	return ($DIG_VERSION[0] >= 9 && $DIG_VERSION[1] >= 3 && $DIG_VERSION[2] >= 0 ? 'recurse' : 'recursive');
}

sub is_ipv4addr {
	return ($_[0] =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
}

### this is about as good as we can get here, since IPv6 addresses can have all different
### kinds of format:
sub is_ipv6addr {
	return ($_[0] =~ /^[A-F0-9:]+$/);
}

sub context_menu {
	my ($model, $iter) = $glade->get_widget('output')->get_selection->get_selected;

	return 1 if (!defined($model) && !defined($iter));

	my ($host, $ttl, $class, $type, $answer) = $model->get($iter);
	$type = uc($type);
	$answer =~ s/\.$//;

	return 1 if ($type eq 'SOA' || $answer eq '');

	my $menu = Gtk2::Menu->new;

	my ($label, $callback);
	if ($type eq 'A') {
		$label = sprintf(gettext("Do a PTR query on %s"), $answer);
		$callback = sub {
			$glade->get_widget('query_combo')->entry->set_text($answer);
			$glade->get_widget('type_combo')->set_active($PTR_ROW_ID);
			$glade->get_widget('query_button')->clicked;
		}

	} elsif ($type eq 'MX') {
		my ($priority, $exchanger) = split(/\s+/, $answer, 2);

		if (is_ipv4addr($exchanger)) {
			$label = sprintf(gettext("Do a PTR query on '%s'"), $exchanger);
			$callback = sub {
				$glade->get_widget('query_combo')->entry->set_text($exchanger);
				$glade->get_widget('type_combo')->set_active($PTR_ROW_ID);
				$glade->get_widget('query_button')->clicked;
			};
		} else {
			$label = sprintf(gettext("Put '%s' into Query box"), $exchanger);
			$callback = sub {
				$glade->get_widget('query_combo')->entry->set_text($exchanger);
				$glade->get_widget('type_combo')->set_active($ANY_ROW_ID);
			};

		}

	} elsif ($type eq 'AAAA') {
		$label = sprintf(gettext("Do a PTR query on '%s'"), $answer);
		$callback = sub {
			$glade->get_widget('query_combo')->entry->set_text($answer);
			$glade->get_widget('type_combo')->set_active($PTR_ROW_ID);
			$glade->get_widget('query_button')->clicked;
		};

	} elsif ($type eq 'NS') {
		$label = sprintf(gettext("Put '%s' into Server box"), $answer);
		$callback = sub {
			$glade->get_widget('server_combo')->entry->set_text($answer);
		};

	} else {
		$label = sprintf(gettext("Put '%s' into Query box"), $answer);
		$callback = sub {
			$glade->get_widget('query_combo')->entry->set_text($answer);
			$glade->get_widget('type_combo')->set_active($ANY_ROW_ID);
		};

	}
	my $item = Gtk2::ImageMenuItem->new_with_label($label);
	$item->set_image(Gtk2::Image->new_from_pixbuf($glade->get_widget('main_window')->render_icon('gtk-jump-to', 'menu')));
	$item->signal_connect('activate', $callback) if (defined($callback));

	$menu->append($item);

	$menu->show_all;
	$menu->popup(undef, undef, undef, undef, 3, undef);

	return 1;
}

sub save_output {
	my $chooser = Gtk2::FileChooserDialog->new(
		gettext('Save as...'),
		$glade->get_widget('main_window'),
		'save',
		'gtk-cancel'	=> 'cancel',
		'gtk-ok' 	=> 'ok',
	);
	$chooser->set_modal(1);
	$chooser->signal_connect('response', sub {
		my $file = $chooser->get_filename;
		$chooser->destroy;
		if ($_[1] eq 'ok') {
			$SAVE_TARGET = $file;
			if (-e $file) {
				$glade->get_widget('overwrite_dialog_title_label')->set_markup(sprintf($OVERWRITE_DIALOG_MARKUP, $file));
				$glade->get_widget('overwrite_dialog')->show_all;

			} else {
				write_output($SAVE_TARGET);

			}
		}
	});
	$chooser->show_all;
	return 1;
}

sub set_status {
	my $text = shift;
	$glade->get_widget('status')->push($glade->get_widget('status')->get_context_id('default'), $text);
}

sub write_output {
	my $file = shift;

	if (!open(FILE, ">$file")) {
		my $dialog = Gtk2::MessageDialog->new(undef, 'modal', 'error', 'ok', sprintf(gettext("Error opening '%s': %s", $file, $!)));
		$dialog->signal_connect('response', sub { $dialog->destroy });
		$dialog->run;

	} else {
		print FILE $glade->get_widget('raw_response')->get_buffer->get_text(
			$glade->get_widget('raw_response')->get_buffer->get_start_iter,
			$glade->get_widget('raw_response')->get_buffer->get_end_iter,
			0
		);
		close(FILE);
	}

	return 1;
}

sub get_current_dir {
	if ($OSNAME eq 'MSWin32') {
		my $dir;
		eval {
			require Win32;
			$dir = Win32::GetCwd();
		};
		return ($@ ? undef : $dir);

	} else {
		return $ENV{PWD};

	}
}

sub on_dig_locator_close {
	exit 0;
}

sub on_dig_locator_response {
	my ($dialog, $response, $glade) = @_;
	if ($response ne 'ok') {
		exit 0;

	} else {
		my $dig = $glade->get_widget('dig_locator_chooser')->get_filename;
		if (!-e $dig) {
			$dialog->run;

		} else {
			$glade->get_widget('dig_locator')->hide;
			$OPTIONS->{dig} = $dig;
			save_options();
			return 1;

		}

	}
}
