package functions;

# This file is part of sqlsus
#
# Copyright (c) 2008-2011 Jérémy Ruffet (sativouf)
# http://sqlsus.sourceforge.net/
#
# Licensed under GPLv3+: GNU GPL version 3 or later
# See LICENSE file or http://www.gnu.org/licenses/gpl.html

use strict;
use warnings;

use Unicode::Normalize;
use Time::HiRes qw/sleep/;
use constant LENGTH_URL => 1;
use constant LENGTH_INJVAR => 2;

##################################
#-- ask a question to the user
sub question {
	my $question = shift;
	my @choices = @_;
	my $answer = "";
	do {
		print "$question [" . join ("|", @choices) . "] ? ";
		$answer = <STDIN>;
		return unless defined $answer;
		chomp $answer;
	} until grep(/$answer/i, @choices);
	return lc $answer;
}   

##################################
sub length_above_max_sendable {
	my $inject_callback = shift;
	my @args = @_;

	if ($conf::max_inj_length) {
		return &$inject_callback(@args, LENGTH_INJVAR) > $conf::max_inj_length;
	} elsif ($conf::max_url_length) {
		return &$inject_callback(@args, LENGTH_URL) > $conf::max_url_length;
	}
	return 0;
}

##################################
sub get_injvar_length {
	my $url = shift;
	
	$url =~ s/(%([a-fA-F0-9]{2}))/chr(hex $2)/eg;

	my @args = split /[\?&][^\?&=]+?=/, $url;
	shift @args;
	my $max_length = 0;
	for (@args) {
		$max_length = length($_) if length($_) > $max_length;
	}
	return $max_length;
}


#-- hit the server with a GET/POST request
# RETURN: 
#   if dry_run : $length_request
#   else : return value of $browser->get/post
##################################
sub get_url {
	my ($url, $form, $dry_run) = @_;
	my $res;
	my $hits = 0;

	$url =~ s# #/**/#g if $conf::convert_spaces;
	# will be done anyway, so better do it now to calculate an accurate length_request
	$url =~ s# #%20#g;
	$url =~ s/#/%23/g;
	$url =~ s/\+/%2B/g;

	my $length_request;
	my $length_injvar;
	# POST
	if ($form and $form ne LENGTH_URL and $form ne LENGTH_INJVAR) {
		my %hash = %{$form};
		if (not $dry_run) {
			my $post_content;
			for (keys %hash) { $post_content .= "  [$_]=> $hash{$_} " }
			&msg::debug("[POST] $url : $post_content");
		}
		# full_url is only used for length calculation purpose
		my $full_url = $url;
		for (keys %hash) { 
			my $arg = $hash{$_};
			# &get_injvar_length may wrongly cut otherwise
			$arg =~ s/[\?&=]/x/g;
			$full_url .= "?$_=$arg";
		}
		$length_request = length($full_url);
		$length_injvar = &get_injvar_length($full_url);
		return $length_injvar if $dry_run and ($dry_run == LENGTH_INJVAR);
		return $length_request if $dry_run and ($dry_run == LENGTH_URL);
		do {
			$res = $main::browser->post($url, \%{$form});
			&hits_add();
		} while (&is_error_code($res->code) and ($hits++ < $conf::http_error_retries) and not &main::is_interrupted());
	# GET
	} else {
		&msg::debug("[GET] $url") unless $dry_run;
		$length_request = length($url);
		$length_injvar = &get_injvar_length($url);
		return $length_injvar if $dry_run and ($dry_run == LENGTH_INJVAR);
		return $length_request if $dry_run and ($dry_run == LENGTH_URL);
		do {
			$res = $main::browser->get($url);
			&hits_add();
		} while (&is_error_code($res->code) and ($hits++ < $conf::http_error_retries) and not &main::is_interrupted());
	}
	return if &main::is_interrupted();
	if ($hits == $conf::http_error_retries) {
		&msg::error("http_error_retries hit");
	}
	if (($conf::max_url_length > 0) and ($length_request > $conf::max_url_length)) {
		&msg::error("max_url_length exceeded (requested URL is " . $length_request . " bytes long), this will probably fail.");
	}
	if (($conf::max_inj_length > 0) and ($length_injvar > $conf::max_inj_length)) {
		&msg::error("max_inj_length exceeded (requested injection point is " . $length_injvar  . " bytes long), this will probably fail.");
	}
	sleep($conf::sleep_after_hit);
	return($res);
}

##################################
#-- show / set global variables
# "set" alone will display all the set variables
sub set_var {
	my $query_args = shift;
	my ($var, @values) = split /\s+/, $query_args;

	$var = "" unless $var;
	my @to_print;

	if ($var =~ /^(|database|db)$/i) { 
		$main::database = join('', @values) if @values;
		if (defined $main::database) {
			$main::database =~ s/^(['"])(.*)\1$/$2/;
			push @to_print, "database = \"$main::database\"";
		}
	}
	if ($var =~ /^(|columns)$/i) { 
		# '' or "" to clear
		@conf::columns = split /[ ,]/, join ' ', grep (!/^(['"])\1$/, @values) if @values;
		push @to_print, "columns = " . join (",", @conf::columns) . "";
	}
	if ($var =~ /^(|binary)$/i) { 
		$conf::binary = join('', @values) if @values;
		if ($conf::binary) {
			$conf::hex_start = "HEX(BINARY ";
			$conf::hex_end = ")";
		} else {
			$conf::hex_start = $conf::hex_end = "";
		}
		push @to_print, "binary = $conf::binary";
	}
	if ($var =~ /^(|blind_max_length)$/i) { 
		$conf::blind_max_length = join('', @values) if @values;
		push @to_print, "blind_max_length = $conf::blind_max_length";
	}
	if ($var =~ /^(|proxy)$/i) { 
		$conf::proxy = join('', @values) if @values;
		$conf::proxy =~ s/^(['"])(.*)\1$/$2/;
		&functions::load_proxy_in_browser;
		push @to_print, "proxy = \"$conf::proxy\"";
	}
	if ($var =~ /^(|cookie)$/i) { 
		if ($conf::use_cookie_jar) {
			$conf::cookie = join('', @values) if @values;
			$conf::cookie =~ s/^(['"])(.*)\1$/$2/;
			&set_cookie($conf::cookie);
			push @to_print, "cookie = \"$conf::cookie\"";
		} else {
			push @to_print, "cookie = DISABLED_BY_CONFIG";
		}
	}
	if ($var =~ /^(|debug)$/i) { 
		$conf::debug = join('', @values) if @values;
		push @to_print, "debug = $conf::debug";
	}
	if ($var =~ /^(|max_returned_length)$/i) { 
		$conf::max_returned_length = join('', @values) if @values;
		push @to_print, "max_returned_length = $conf::max_returned_length";
	}
	if ($var =~ /^(|max_url_length)$/i) { 
		$conf::max_url_length = join('', @values) if @values;
		push @to_print, "max_url_length = $conf::max_url_length";
	}
	if ($var =~ /^(|max_inj_length)$/i) { 
		$conf::max_inj_length = join('', @values) if @values;
		push @to_print, "max_inj_length = $conf::max_inj_length";
	}
	if ($var =~ /^(|max_subqueries)$/i) { 
		$conf::max_subqueries = join('', @values) if @values;
		push @to_print, "max_subqueries = $conf::max_subqueries";
	}
	if ($var =~ /^(|processes)$/i) { 
		$conf::processes = join('', @values) if @values;
		push @to_print, "processes = $conf::processes";
	}
	if ($var =~ /^(|sleep_after_hit)$/i) { 
		$conf::sleep_after_hit = join('', @values) if @values;
		push @to_print, "sleep_after_hit = $conf::sleep_after_hit";
	}
	if ($var =~ /^(|table_prefix)$/i) { 
		$conf::table_prefix = join('', @values) if @values;
		$conf::table_prefix =~ s/^(['"])(.*)\1$/$2/;
		push @to_print, "table_prefix = \"$conf::table_prefix\"";
	}
	if ($var =~ /^(|http_error_retries)$/i) { 
		$conf::http_error_retries = join('', @values) if @values;
		push @to_print, "http_error_retries = $conf::http_error_retries";
	}
	if ($var =~ /^(|document_root)$/i) { 
		$conf::document_root = join('', @values) if @values;
		$conf::document_root =~ s/^(['"])(.*)\1$/$2/;
		push @to_print, "document_root = \"$conf::document_root\"";
	}
	if ($var =~ /^(|crawler_depth)$/i) { 
		$conf::crawler_depth = join('', @values) if @values;
		push @to_print, "crawler_depth = $conf::crawler_depth";
	}
	if ($var =~ /^(|uploader)$/i) { 
		$conf::uploader = join('', @values) if @values;
		$conf::uploader =~ s/^(['"])(.*)\1$/$2/;
		push @to_print, "uploader = \"$conf::uploader\"";
	}
	if ($var =~ /^(|backdoor)$/i) { 
		$conf::backdoor = join('', @values) if @values;
		$conf::backdoor =~ s/^(['"])(.*)\1$/$2/;
		push @to_print, "backdoor = \"$conf::backdoor\"";
	}

	if (@to_print) {
		print join ("\n", @to_print ). "\n";
	} else {
		&msg::error("No such variable : $var");
	}
}

##################################
#-- converts string to 0xHEX
sub string_to_hex {
	my $str = shift;
	$str =~ s/(.)/ sprintf '%02x', ord $1 /seg;
	return "0x" . $str;
}

#-- and the opposite..
sub hex_to_string {
	(my $str = shift) =~ s/([a-fA-F0-9]{2})/chr(hex $1)/eg;
	$str =~ s/^0x//;
	return $str;
}


##################################
#-- gen random string
sub generate_random_string {
	my $length = shift;
	my @keyspace = @_;

	my $random_string;
	for (1..$length) {
		$random_string .= $keyspace[rand @keyspace];
	}
	return $random_string;
}

##################################
sub is_error_code {
	my $code = shift;

	if (grep($_ eq $code, @conf::http_error_codes)) {
		&msg::debug("HTTP Error: $code") if not &main::is_interrupted();
		return 1;
	} else {
		return 0;
	}
}

##################################
#-- find the tables that have a column that matches the given pattern
sub find_tables {
	my $pattern = shift;

	if ($pattern) {
		$pattern =~ s/^(['"])(.*)\1/$2/;
		if (defined $main::database) {
			my @result = &select::mass_query("SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema = \"$main::database\" AND column_name LIKE \"$pattern\"", 1);
			if (@result) {
				&functions::print_array_in_a_box(&hits_counter(), [ qw(table_name) ], @result);
			} else {
				&msg::info("No table found with a column matching this pattern");
			}
		} else {
			&msg::error("Current database not defined, use \"start\" first");
		}
	} else {
		print "Please specify a column pattern\n";
	}
}

##################################
sub print_table_in_a_box {
	my $hits = shift;
	my $database = shift;
	my $result_table = shift;

	my @header_row = &db::get_header($result_table);

	if (not $result_table or &db::count_rows($database, $result_table) == 0) {
		print "Empty set\n\n";
		return 0;
	}

	my $hits_info = "";
	$hits_info = "($hits hit" . ($hits == 1 ? "" : "s") . ")" if defined $hits;

	my @cols_size = ();
	my $header_decoration_line;
	my $header_line;

	# Get max size for every column
	# set it to header for the minimum value
	for (@header_row) {
		push @cols_size, length $_ if $_;
	}

	my $i = 0;
	for my $col (@header_row) {
		my $longest = &db::longest($database, $result_table, $col);
		($cols_size[$i]) = $longest if not defined $cols_size[$i] or $longest > $cols_size[$i];
		$i++;
	}
	for my $i (0..$#cols_size) {
		$header_decoration_line .= "+" . "-" x ($cols_size[$i] + 2);
		$header_line .= "| " . $header_row[$i] . " " x ($cols_size[$i] - length ($header_row[$i]) + 1);
	}
	print "$header_decoration_line" . "+\n";
	print "$header_line" . "|\n";
	print "$header_decoration_line" . "+\n";

	my $dbh = DBI->connect("dbi:SQLite:dbname=$database","","");
	my $sql = "SELECT * FROM $result_table ORDER BY ABS(row)";
	my $sth = $dbh->prepare($sql);
	$sth->execute();
	while (my @row = $sth->fetchrow_array()) {
		for my $i (0..$#cols_size) {
			my $item = $row[$i+1];
			$item = "" if not defined $item;
			$item =~ s/\x06/$conf::null_substitute/g;
			# my $padding_len = length(NFD $item) - length($item);
			my $padding_len = 0;
			if ($item =~ /^-?[\d\.~]+$/) {
				print "| " . " " x ($cols_size[$i] - length ($item) + $padding_len) . $item . " ";
			} else {
				print "| " . $item . " " x ($cols_size[$i] - length ($item) + $padding_len + 1);
			}
		}
		print "|\n";
	}

	print "$header_decoration_line" . "+\n";
	my $rows = $sth->rows;
	my $rows_word = "row";
	$rows_word .= "s" if $rows > 1;
	print "$rows $rows_word in set $hits_info\n\n";

	return 1;
}

##################################
sub print_array_in_a_box {
	print &array_in_a_box(@_);
}

##################################
sub array_in_a_box {
	my $hits = shift;
	my @header_row = @{ shift(@_) };
	my @contents = @_;

	my $result;
	my $hits_info = "";
	$hits_info = "($hits hit" . ($hits == 1 ? "" : "s") . ")" if defined $hits;

	if (scalar (@contents) == 0) {
		return "Empty set\n\n";
	}
	my @cols_size = ();
	my $header_decoration_line;
	my $header_line;

	# Get max size for every column
	# set it to header to start
	for (@header_row) {
		push @cols_size, length $_;
	}
	for (my $i = 0; $i < $#contents + 1; $i += $#cols_size + 1) {
		for my $j (0..$#cols_size) {
			$contents[$i+$j] = "" unless defined $contents[$i+$j];
			my $len = length $contents[$i+$j];
			if ($cols_size[$j] < $len) {
				$cols_size[$j] = $len;
			}
		}
	}
	for my $i (0..$#cols_size) {
		$header_decoration_line .= "+" . "-" x ($cols_size[$i] + 2);
		$header_line .= "| " . $header_row[$i] . " " x ($cols_size[$i] - length ($header_row[$i]) + 1);
	}
	$result .= "$header_decoration_line" . "+\n";
	$result .= "$header_line" . "|\n";
	$result .= "$header_decoration_line" . "+\n";
	for (my $i = 0; $i < $#contents + 1; $i += $#cols_size + 1) {
		for my $j (0..$#cols_size) {
			my $item = $contents[$i+$j];
			$item = "" if not defined $item;
			$item =~ s/\x06/$conf::null_substitute/g;
			# my $padding_len = length(NFD $item) - length($item);
			my $padding_len = 0;
			if ($item =~ /^-?[\d\.~]+$/) {
				$result .= "| " . " " x ($cols_size[$j] - length ($item) + $padding_len) . $item . " ";
			} else {
				$result .= "| " . $item . " " x ($cols_size[$j] - length ($item) + $padding_len + 1);
			}
		}
		$result .= "|\n";
	}

	$result .= "$header_decoration_line" . "+\n";
	my $rows = int (($#contents + 1) / ($#cols_size + 1));
	my $rows_word = "row";
	$rows_word .= "s" if $rows > 1;
	$result .= "$rows $rows_word in set $hits_info\n\n";

	return $result;
}

##################################
sub set_cookie {
	my $cookie = shift;

	if (not $conf::use_cookie_jar) {
		&msg::error("conf::use_cookie_jar set to 0, not setting cookie.");
		return 0;
	}
	$main::browser->cookie_jar->clear;
	$cookie .= ";" if ($cookie =~ /.*=.*[^;]\s*$/);
	for (split /\s*;\s*/, $cookie) {
		my ($key, $value) = /(.*?)=(.*)/;
		my $tld = "";
		$tld = $1 if ($main::server =~ /([^\.]+)$/);
		$main::browser->cookie_jar->set_cookie(0, $key, $value, "/", ".$tld",,,,,);
	}
}

##################################
sub hits_counter {
	$main::control_child->send("hits print");
	$main::control_child->recv(my $buf, 100000);
	return $buf;
}

##################################
sub hits_reset {
	$main::control_child->send("hits reset");
}

##################################
sub hits_add {
	$main::control_child->send("hits +");
}

##################################
sub interrupt_hit {
	$main::control_child->send("interrupt +");
}

##################################
sub interrupt_reset {
	$main::control_child->send("interrupt reset");
}

##################################
sub interrupt {
	$main::control_child->send("interrupt print");
	$main::control_child->recv(my $buf, 100);
	return $buf;
}

##################################
sub my_fork {
	$SIG{'INT'} = 'IGNORE';
	return fork();
}

##################################
sub load_proxy_in_browser {
	if ($conf::proxy) {
		if ($conf::proxy =~ /^socks/) {
			eval { require LWP::Protocol::socks };
			if ($@) { &msg::warning("socks proxy disabled, please install LWP::Protocol::socks (\"cpan LWP::Protocol::socks\" or \"apt-get install liblwp-protocol-socks-perl\")") }
		}
		eval { $main::browser->proxy([qw/ http https /], $conf::proxy) };
		if ($@) { 
			&msg::error("Incorrect proxy settings detected, no proxy will be used.");
			$main::browser->proxy([qw/ http https /], "");
		};
	}
}

1
