#!/usr/bin/perl
##########################################################################
# Tentacle Server
# See http://www.openideas.info/wiki for protocol description.
# Tentacle have IANA assigned port tpc/41121 as official port.
##########################################################################
# Copyright (c) 2007-2008  Ramon Novoa  <rnovoa@artica.es>
# Copyright (c) 2005-2010 Artica Soluciones Tecnologicas S.L
#
# tentacle_server.pl	Tentacle Server. See http://www.openideas.info/wiki for
#                       protocol description.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
# 
# 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 General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
##########################################################################

package tentacle::server;
=head1 NAME

tentacle_server - Tentacle Server

=head1 VERSION

Version 0.5.0

=head1 USAGE

tentacle_server B<< -s F<storage_directory> >> [I<options>]

=head1 DESCRIPTION

B<tentacle_server(1)> is a server for B<tentacle>, a B<client/server> file transfer protocol that aims to be:

=over

=item    * Secure by design.

=item    * Easy to use.

=item    * Versatile and cross-platform. 

=back 

Tentacle was created to replace more complex tools like SCP and FTP for simple file transfer/retrieval, and switch from authentication mechanisms like .netrc, interactive logins and SSH keys to X.509 certificates. Simple password authentication over a SSL secured connection is supported too.

The client and server (B<TCP port 41121>) are designed to be run from the command line or called from a shell script, and B<no configuration files are needed>. 

=cut

use strict;
use warnings;
use Getopt::Std;
use IO::Select;
use threads;
use Thread::Semaphore;
use POSIX ":sys_wait_h";
use Time::HiRes qw(usleep);

# Constants for Win32 services.
use constant WIN32_SERVICE_STOPPED => 0x01;
use constant WIN32_SERVICE_RUNNING => 0x04;

my $t_libwrap_installed = eval { require Authen::Libwrap } ? 1 : 0;

if ($t_libwrap_installed) {
	Authen::Libwrap->import( qw( hosts_ctl STRING_UNKNOWN ) );
}

# Log messages, 1 enabled, 0 disabled
my $t_log = 0;

my $SOCKET_MODULE =
	eval { require IO::Socket::INET6 } ? 'IO::Socket::INET6'
      : eval { require IO::Socket::INET }  ? 'IO::Socket::INET'
      : die $@;

# Service name for Win32.
my $SERVICE_NAME="Tentacle Server";

# Service parameters.
my $SERVICE_PARAMS=join(' ', @ARGV);

# Program version
our $VERSION = '0.5.0';

# IPv4 address to listen on
my @t_addresses = ('0', '0.0.0.0');

# Block size for socket read/write operations in bytes
my $t_block_size = 1024;

# Client socket
my $t_client_socket;

# Run as daemon, 1 true, 0 false
my $t_daemon = 0;

# Storage directory
my $t_directory = '';

# Filters
my @t_filters;

# String containing quoted invalid file name characters
my $t_invalid_chars = '\?\[\]\/\\\=\+\<\>\:\;\'\,\*\~';

# Maximum number of simultaneous connections
my $t_max_conn = 10;

# Maximum file size allowed by the server in bytes
my $t_max_size = 2000000;

# File overwrite, 1 enabled, 0 disabled
my $t_overwrite = 0;

# Port to listen on
my $t_port = 41121;

# Server password
my $t_pwd = '';

# Do not output error messages, 1 enabled, 0 disabled
my $t_quiet = 0;

# Number of retries for socket read/write operations
my $t_retries = 3;

# Select handler
my $t_select;

# Semaphore
my $t_sem :shared;

# Server socket
my @t_server_sockets;

# Server select handler
my $t_server_select;

# Use SSL, 1 true, 0 false
my $t_ssl = 0;

# SSL ca certificate file
my $t_ssl_ca = '';

# SSL certificate file
my $t_ssl_cert = '';

# SSL private key file
my $t_ssl_key = '';

# SSL private key password
my $t_ssl_pwd = '';

# Timeout for socket read/write operations in seconds
my $t_timeout = 1;

# Address to proxy client requests to
my $t_proxy_ip = undef;

# Port to proxy client requests to
my $t_proxy_port = 41121;

# Proxy socket
my $t_proxy_socket;

# Proxy selected handler
my $t_proxy_select;

# Use libwrap, 1 true, 0 false
my $t_use_libwrap = 0;

# Program name for libwrap
my $t_program_name = $0;
$t_program_name =~ s/.*\///g;

################################################################################
## SUB print_help
## Print help screen.
################################################################################
sub print_help {
	$" = ',';

	print ("Usage: $0 -s <storage directory> [options]\n\n");
	print ("Tentacle server v$VERSION. See http://www.openideas.info/wiki for protocol description.\n\n");
	print ("Options:\n");
	print ("\t-a ip_addresses\tIP addresses to listen on (default @t_addresses).\n");
	print ("\t               \t(Multiple addresses separated by comma can be defined.)\n");
	print ("\t-c number\tMaximum number of simultaneous connections (default $t_max_conn).\n");
	print ("\t-d\t\tRun as daemon.\n");
	print ("\t-e cert\t\tOpenSSL certificate file. Enables SSL.\n");
	print ("\t-f ca_cert\tVerify that the peer certificate is signed by a ca.\n");
	print ("\t-h\t\tShow help.\n");
	print ("\t-i\t\tFilters.\n");
	print ("\t-k key\t\tOpenSSL private key file.\n");
	print ("\t-m size\t\tMaximum file size in bytes (default ${t_max_size}b).\n");
	print ("\t-o\t\tEnable file overwrite.\n");
	print ("\t-p port\t\tPort to listen on (default $t_port).\n");
	print ("\t-q\t\tQuiet. Do now print error messages.\n");
	print ("\t-r number\tNumber of retries for network opertions (default $t_retries).\n");
	print ("\t-S (install|uninstall|run) Manage the win32 service.\n");
	print ("\t-t time\t\tTime-out for network operations in seconds (default ${t_timeout}s).\n");
	print ("\t-v\t\tBe verbose.\n");
	print ("\t-w\t\tPrompt for OpenSSL private key password.\n");
	print ("\t-x pwd\t\tServer password.\n");
	print ("\t-b ip_address\tProxy requests to the given address.\n");
	print ("\t-g port\t\tProxy requests to the given port.\n");
	print ("\t-T\t\tEnable tcpwrappers support.\n");
	print ("\t  \t\t(To use this option, 'Authen::Libwrap' should be installed.)\n\n");
}

################################################################################
## SUB daemonize
## Turn the current process into a daemon.
################################################################################
sub daemonize {
	my $pid;

	require POSIX;

	chdir ('/') || error ("Cannot chdir to /: $!.");
	umask 0;

	open (STDIN, '/dev/null') || error ("Cannot read /dev/null: $!.");

	# Do not be verbose when running as a daemon
	open (STDOUT, '>/dev/null') || error ("Cannot write to /dev/null: $!.");
	open (STDERR, '>/dev/null') || error ("Cannot write to /dev/null: $!.");

	# Fork
	$pid = fork ();
	if (! defined ($pid)) {
		error ("Cannot fork: $!.");
	}

	# Parent
	if ($pid != 0) {
		exit;
	}

	# Child
	POSIX::setsid () || error ("Cannot start a new session: $!.");
}

################################################################################
## SUB parse_options
## Parse command line options and initialize global variables.
################################################################################
sub parse_options {
	my %opts;
	my $tmp;
	my @t_addresses_tmp;

	# Get options
	if (getopts ('a:b:c:de:f:g:hi:k:m:op:qr:s:S:t:Tvwx:', \%opts) == 0 || defined ($opts{'h'})) {
		print_help ();
		exit 1;
	}

	# The Win32 service must be installed/uninstalled without checking other parameters.
	if (defined ($opts{'S'})) {
		my $service_action = $opts{'S'};
		if ($^O ne 'MSWin32') {
			error ("Windows services are only available on Win32.");
		} else {
			eval "use Win32::Daemon";
			die($@) if ($@);

			if ($service_action eq 'install') {
				install_service();
			} elsif ($service_action eq 'uninstall') {
				uninstall_service();
			}
		}
	}

	# Address
	if (defined ($opts{'a'})) {
		@t_addresses = ();
		@t_addresses_tmp = split(/,/, $opts{'a'});
		
		foreach my $t_address (@t_addresses_tmp) {
			$t_address =~ s/^ *(.*?) *$/$1/;
			if (($t_address ne '0') && 
				($t_address !~ /^[a-zA-Z\.]+$/ && ($t_address  !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
					|| $1 < 0 || $1 > 255 || $2 < 0 || $2 > 255
					|| $3 < 0 || $3 > 255 || $4 < 0 || $4 > 255)) &&
				($t_address !~ /^[0-9a-f:]+$/o)) {
					error ("Address $t_address is not valid.");
			}
			push @t_addresses, $t_address;
		}
	}
	
	# Maximum simultaneous connections
	if (defined ($opts{'c'})) {
		$t_max_conn = $opts{'c'};
		if ($t_max_conn !~ /^\d+$/ || $t_max_conn < 1) {
			error ("Invalid number of maximum simultaneous connections.");
		}
	}

	# Run as daemon
	if (defined ($opts{'d'})) {
		if ($^ eq 'MSWin32') {
			error ("-d flag not available for this OS.");
		}

		$t_daemon = 1;
	}

	# Enable SSL
	if (defined ($opts{'e'})) {

		require IO::Socket::SSL;

		$t_ssl_cert = $opts{'e'};
		if (! -f $t_ssl_cert) {
			error ("File $t_ssl_cert does not exist.");
		}

		$t_ssl = 1;
	}

	# Verify peer certificate
	if (defined ($opts{'f'})) {
		$t_ssl_ca = $opts{'f'};
		if (! -f $t_ssl_ca) {
			error ("File $t_ssl_ca does not exist.");
		}
	}

	# Filters (regexp:dir;regexp:dir...)
	if (defined ($opts{'i'})) {
		my @filters = split (';', $opts{'i'});
		foreach my $filter (@filters) {
			my ($regexp, $dir) = split (':', $filter);
			next unless defined ($regexp) && defined ($dir);

			# Remove any trailing /
			my $char = chop ($dir);
			$dir .= $char if ($char) ne '/';

			push(@t_filters, [$regexp, $dir]);
		}
	}

	# SSL private key file
	if (defined ($opts{'k'})) {
		$t_ssl_key = $opts{'k'};
		if (! -f $t_ssl_key) {
			error ("File $t_ssl_key does not exist.");
		}
	}

	# Maximum file size
	if (defined ($opts{'m'})) {
		$t_max_size = $opts{'m'};
		if ($t_max_size !~ /^\d+$/ || $t_max_size < 1) {
			error ("Invalid maximum file size.");
		}
	}

	# File overwrite
	if (defined ($opts{'o'})) {
		$t_overwrite = 1;
	}

	# Port
	if (defined ($opts{'p'})) {
		$t_port = $opts{'p'};
		if ($t_port !~ /^\d+$/ || $t_port < 1 || $t_port > 65535) {
			error ("Port $t_port is not valid.");
		}
	}

	# Quiet mode
	if (defined ($opts{'q'})) {
		$t_quiet = 1;
	}

	# Retries
	if (defined ($opts{'r'})) {
		$t_retries = $opts{'r'};
		if ($t_retries !~ /^\d+$/ || $t_retries < 1) {
			error ("Invalid number of retries for network operations.");
		}
	}

	# Storage directory
	if (defined ($opts{'s'})) {

		$t_directory = $opts{'s'};
		
		# Check that directory exists
		if (! -d $t_directory) {
			error ("Directory $t_directory does not exist.");
		}

		# Check directory permissions
		if (! -w $t_directory) {
			error ("Cannot write to directory $t_directory.");
		}

		# Remove the trailing / if present
		$tmp = chop ($t_directory);
		if ($tmp ne '/') {
			$t_directory .= $tmp;
		}
	}
	else {
		if (! defined($opts{'b'})) {
			print_help ();
			exit 1;
		}
	}

	# Timeout
	if (defined ($opts{'t'})) {
		$t_timeout = $opts{'t'};
		if ($t_timeout !~ /^\d+$/ || $t_timeout < 1) {
			error ("Invalid timeout for network operations.");
		}
	}

	# Be verbose
	if (defined ($opts{'v'})) {
		$t_log = 1;
	}

	# SSL private key password
	if (defined ($opts{'w'})) {
		$t_ssl_pwd = ask_passwd ("Enter private key file password: ", "Enter private key file password again for confirmation: ");
	}

	# Server password
	if (defined ($opts{'x'})) {
		$t_pwd = $opts{'x'};
	}
	
	#Proxy IP address
	if (defined ($opts{'b'})) {
		$t_proxy_ip = $opts{'b'};
		if ($t_proxy_ip !~ /^[a-zA-Z\.]+$/ && ($t_proxy_ip  !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
			|| $1 < 0 || $1 > 255 || $2 < 0 || $2 > 255
			|| $3 < 0 || $3 > 255 || $4 < 0 || $4 > 255) &&
			$t_proxy_ip !~ /^[0-9a-f:]+$/o) {
			error ("Proxy address $t_proxy_ip is not valid.");
		}		
	}
	
	# Proxy Port
	if (defined ($opts{'g'})) {
		$t_proxy_port = $opts{'g'};
		if ($t_proxy_port !~ /^\d+$/ || $t_proxy_port < 1 || $t_proxy_port > 65535) {
			error ("Proxy port $t_port is not valid.");
		}
	}	

	# TCP wrappers support
	if (defined ($opts{'T'})) {
		if ($t_libwrap_installed) {
			$t_use_libwrap = 1;
		} else {
			error ("Authen::Libwrap is not installed.");
		}
	}

	# Win32 service management
	if (defined ($opts{'S'})) {
		my $service_action = $opts{'S'};
		if ($^O ne 'MSWin32') {
			error ("Windows services are only available on Win32.");
		} else {
			eval "use Win32::Daemon";
			die($@) if ($@);

			if ($service_action eq 'run') {
				Win32::Daemon::RegisterCallbacks({
			        start       =>  \&callback_start,
			        running     =>  \&callback_running,
			        stop        =>  \&callback_stop,
				});
				Win32::Daemon::StartService();
				exit 0;
			} else {
				error("Unknown action: $service_action");
			}
		}
	}
}

################################################################################
## SUB start_proxy
## Open the proxy server socket.
################################################################################
sub start_proxy {

	# Connect to server
	$t_proxy_socket = $SOCKET_MODULE->new (
	    PeerAddr => $t_proxy_ip,
		PeerPort => $t_proxy_port,
	);

	if (! defined ($t_proxy_socket)) {
		error ("Cannot connect to $t_proxy_ip on port $t_proxy_port: $!.");
	}
	
	# Create proxy selector
	$t_proxy_select = IO::Select->new ();
	$t_proxy_select->add ($t_proxy_socket);
	
}

################################################################################
## SUB start_server
## Open the server socket.
################################################################################
sub start_server {

	my $t_server_socket;

	foreach my $t_address (@t_addresses) {

		$t_server_socket = $SOCKET_MODULE->new (
			Listen    => $t_max_conn,
			LocalAddr => $t_address,
			LocalPort => $t_port,
			Proto     => 'tcp',
			ReuseAddr     => 1,
		);

		if (! defined ($t_server_socket)) {
			print_log ("Cannot open socket for address $t_address on port $t_port: $!.");
			next;
		}

		print_log ("Server listening on $t_address port $t_port (press <ctr-c> to stop)");
	
		# Say message if tentacle proxy is enable
		if (defined ($t_proxy_ip)) {
			print_log ("Proxy Mode enable, data will be sent to $t_proxy_ip port $t_proxy_port");	
		}
	
		push @t_server_sockets, $t_server_socket;
	}

	if (!@t_server_sockets) {
		error ("Cannot open socket for all addresses on port $t_port: $!.");
	}
	
	$t_server_select = IO::Select->new();
	foreach my $t_server_socket (@t_server_sockets){
		$t_server_select->add($t_server_socket);
 	}
}

################################################################################
## SUB send_data_proxy
## Send data to proxy socket.
################################################################################
sub send_data_proxy {
	my $data = $_[0];
	my $retries = 0;
	my $size;
	my $total = 0;
	my $written;

	$size = length ($data);

	while (1) {

		# Try to write data to the socket
		if ($t_proxy_select->can_write ($t_timeout)) {

			$written = syswrite ($t_proxy_socket, $data, $size - $total, $total);

			# Write error
			if (! defined ($written)) {
				error ("Connection error from " . $t_proxy_socket->sockhost () . ": $!.");
			}
			
			# EOF
			if ($written == 0) {
				error ("Connection from " . $t_proxy_socket->sockhost () . " unexpectedly closed.");
			}
	
		}

		$total += $written;

		# Check if all data was written
		if ($total == $size) {
			return;
		}

		# Retry
		$retries++;

		# But check for error conditions first
		if ($retries > $t_retries) {
			error ("Connection from " . $t_proxy_socket->sockhost () . " timed out.");
		}
	}
}

################################################################################
## SUB close_proxy
## Close the proxy socket.
################################################################################
sub close_proxy {
	$t_proxy_socket->shutdown (2);
	$t_proxy_socket->close ();
}

################################################################################
## SUB stop_server
## Close the server socket.
################################################################################
sub stop_server {

	foreach my $t_server_socket (@t_server_sockets) {
		$t_server_socket->shutdown (2);
		$t_server_socket->close ();
	}
	print_log ("Server going down");
	
	exit 0;
}

################################################################################
## SUB start_ssl
## Convert the client socket to an IO::Socket::SSL socket.
################################################################################
sub start_ssl {
	my $err;

	if ($t_ssl_ca eq '') {
		IO::Socket::SSL->start_SSL (
			$t_client_socket,
			SSL_cert_file => $t_ssl_cert,
			SSL_key_file => $t_ssl_key,
			SSL_passwd_cb => sub {return $t_ssl_pwd},
			SSL_server => 1,
			# Verify peer
			SSL_verify_mode => 0x01,
		);
	}
	else {
		IO::Socket::SSL->start_SSL (
			$t_client_socket,
			SSL_ca_file => $t_ssl_ca,
			SSL_cert_file => $t_ssl_cert,
			SSL_key_file => $t_ssl_key,
			SSL_passwd_cb => sub {return $t_ssl_pwd},
			SSL_server => 1,
			# Fail verification if no peer certificate exists
			SSL_verify_mode => 0x03,
		);
	}

	$err = IO::Socket::SSL::errstr ();
	if ($err ne '') {
		error ($err);
	}

	print_log ("SSL started for " . $t_client_socket->sockhost ());
}

################################################################################
## SUB accept_connections
## Manage incoming connections.
################################################################################
sub accept_connections {
	my $pid;
	my $t_server_socket;

	# Ignore SIGPIPE errors (happens on FreeBSD when SSL is enabled ¿?)
	$SIG{PIPE} = 'IGNORE';

	# Start server
	start_server ();

	# Initialize semaphore
	$t_sem = Thread::Semaphore->new ($t_max_conn);

	while (1) {
		my @ready = $t_server_select->can_read;
		foreach $t_server_socket (@ready) {

			# Accept connection
			$t_client_socket = $t_server_socket->accept ();

			if (! defined ($t_client_socket)) {
				next if ($! ne ''); # EINTR
				error ("accept: $!.");
			}

			print_log ("Client connected from " . $t_client_socket->peerhost ());

			if ($t_use_libwrap && (! hosts_ctl($t_program_name, $t_client_socket))) {
				print_log ("Connection from " . $t_client_socket->peerhost() . " is closed by tcpwrappers.");
				$t_client_socket->shutdown (2);
				$t_client_socket->close();
			}
			else {

				# Create a new thread and serve the client
				$t_sem->down();
				my $thr = threads->create(\&serve_client);
				if (! defined ($thr)) {
					error ("Error creating thread: $!.");
				}
				$thr->detach();
				$t_client_socket->close ();
			}
		}

		usleep (1000);
	}
}

################################################################################
## SUB serve_client
## Serve a connected client.
################################################################################
sub serve_client() {

	eval {		
		# Add client socket to select queue
		$t_select = IO::Select->new ();
		$t_select->add ($t_client_socket);
			
		# Start SSL
		if ($t_ssl == 1) {
			start_ssl ();
		}
	
		# Authenticate client
		if ($t_pwd ne '') {
			auth_pwd ();
		}
	
		# Check if proxy mode is enable
		if (defined ($t_proxy_ip)) {
			serve_proxy_connection ();	
		} else {
			serve_connection ();
		}
	};

	$t_client_socket->shutdown (2);
	$t_client_socket->close ();
	$t_sem->up();
}

################################################################################
## SUB serve_proxy_connection
## Actuate as a proxy between its client and other tentacle server.
################################################################################
sub serve_proxy_connection {
	
	# We are a proxy! Start a connection to the Tentacle Server.
	start_proxy();

	# Forward data between the client and the server.
	eval {
		while (1) {
			if ($t_select->can_read(0)) {
					my ($read, $data) = recv_data($t_block_size);
					send_data_proxy($data);
			}
			if ($t_proxy_select->can_read(0)) {
					my ($read, $data) = recv_data_proxy($t_block_size);
					send_data($data);
			}
		}
	};

	# Close the connection to the Tentacle Server.
	close_proxy();
}

################################################################################
## SUB serve_connection
## Read and process commands from the client.
################################################################################
sub serve_connection {
	my $command;

	# Read commands
	while ($command = recv_command ($t_block_size)) {
		
		# Client wants to send a file
		if ($command =~ /^SEND <(.*)> SIZE (\d+)$/) {
			print_log ("Request to send file '$1' size ${2}b from " . $t_client_socket->sockhost ());
			recv_file ($1, $2);
		}
		# Client wants to receive a file
		elsif ($command =~ /^RECV <(.*)>$/) {
			print_log ("Request to receive file '$1' from " . $t_client_socket->sockhost ());
			send_file ($1);
		}
		# Quit
		elsif ($command =~ /^QUIT$/) {
			print_log ("Connection closed from " . $t_client_socket->sockhost ());
			last;
		}
		# Unknown command
		else {
			print_log ("Unknown command '$command' from " . $t_client_socket->sockhost ());
			last;
		}
	}
}

################################################################################
## SUB auth_pwd
## Authenticate client with server password.
################################################################################
sub auth_pwd {
	my $client_digest;
	my $command;
	my $pwd_digest;

	require Digest::MD5;
	
	# Wait for password
	$command = recv_command ($t_block_size);
	if ($command !~ /^PASS (.*)$/) {
		error ("Client " . $t_client_socket->sockhost () . " did not authenticate.");
	}
	
	$client_digest = $1;
	$pwd_digest = Digest::MD5::md5 ($t_pwd);
	$pwd_digest = Digest::MD5::md5_hex ($pwd_digest);

	if ($client_digest ne $pwd_digest) {
		error ("Invalid password from " . $t_client_socket->sockhost () . ".");
	}

	print_log ("Client " . $t_client_socket->sockhost () . " authenticated");
	send_data ("PASS OK\n");
}

################################################################################
## SUB recv_file
## Receive a file of size $_[1] and save it in $t_directory as $_[0].
################################################################################
sub recv_file {
	my $base_name = $_[0];
	my $data = '';
	my $file;
	my $size = $_[1];

	# Check file name
	if ($base_name =~ /[$t_invalid_chars]/) {
		print_log ("File '$base_name' size ${size}b from " . $t_client_socket->sockhost () . " has an invalid file name");
		send_data ("SEND ERR\n");
		return;
	}

	# Check file size, empty files are not allowed
	if ($size < 1 || $size > $t_max_size) {
		print_log ("File '$base_name' size ${size}b from " . $t_client_socket->sockhost () . " is too big");
		send_data ("SEND ERR\n");
		return;
	}
	
	# Apply filters
	$file = "$t_directory/" . apply_filters ($base_name) . $base_name;

	# Check if file exists
	if (-f $file && $t_overwrite == 0) {
		print_log ("File '$base_name' size ${size}b from " . $t_client_socket->sockhost () . " already exists");
		send_data ("SEND ERR\n");
		return;
	}

	send_data ("SEND OK\n");

	# Receive file
	$data = recv_data_block ($size);

	# Write it to disk
	open (FILE, "> $file") || error ("Cannot open file '$file' for writing.");
	binmode (FILE);
	print (FILE $data);
	close (FILE);

	send_data ("SEND OK\n");
	print_log ("Received file '$base_name' size ${size}b from " . $t_client_socket->sockhost ());
}

################################################################################
## SUB send_file
## Send a file to the client
################################################################################
sub send_file {
	my $base_name = $_[0];
	my $data = '';
	my $file;
	my $response;
	my $size;

	# Check file name
	if ($base_name =~ /[$t_invalid_chars]/) {
		print_log ("Requested file '$base_name' from " . $t_client_socket->sockhost () . " has an invalid file name");
		send_data ("RECV ERR\n");
		return;
	}
	
	# Apply filters
	$file = "$t_directory/" . apply_filters ($base_name) . $base_name;

	# Check if file exists
	if (! -f $file) {
		print_log ("Requested file '$file' from " . $t_client_socket->sockhost () . " does not exist");
		send_data ("RECV ERR\n");
		return;
	}

	$size = -s $file;
	send_data ("RECV SIZE $size\n");
	
	# Wait for client response
	$response = recv_command ($t_block_size);
	if ($response ne "RECV OK") {
		print_log ("Requested file '$file' from " . $t_client_socket->sockhost () . " not sent");
		return;
	}

	# Send the file
	open (FILE, $file) || error ("Cannot open file '$file' for reading.");
	binmode (FILE);

	while ($data = <FILE>) {
		send_data ($data);
	}

	close (FILE);

	print_log ("Requested file '$file' from " . $t_client_socket->sockhost () . " sent");
}

################################################################################
# Common functions
################################################################################

################################################################################
## SUB print_log
## Print log messages.
################################################################################
sub print_log {

	if ($t_log == 1) {
		print (STDOUT "[log] $_[0]\n");
	}
}

################################################################################
## SUB error
## Print an error and exit the program.
################################################################################
sub error {

	if ($t_quiet == 0) {
		die("[err] $_[0]\n\n");
	}
}

################################################################################
## SUB recv_data_proxy
## Recv data from proxy socket.
################################################################################
sub recv_data_proxy {
	my $data;
	my $read;
	my $retries = 0;
	my $size = $_[0];

	while (1) {

		# Try to read data from the socket
		if ($t_proxy_select->can_read ($t_timeout)) {
			
			# Read at most $size bytes
			$read = sysread ($t_proxy_socket, $data, $size);

			# Read error
			if (! defined ($read)) {
				error ("Read error from " . $t_proxy_socket->sockhost () . ": $!.");
			}
	
			# EOF
			if ($read == 0) {
				error ("Connection from " . $t_proxy_socket->sockhost () . " unexpectedly closed.");
			}
	
			return ($read, $data);
		}

		# Retry
		$retries++;

		# But check for error conditions first
		if ($retries > $t_retries) {
			error ("Connection from " . $t_proxy_socket->sockhost () . " timed out.");
		}
	}
}
################################################################################
## SUB recv_data
## Read data from the client socket. Returns the number of bytes read and the
## string of bytes as a two element array.
################################################################################
sub recv_data {
	my $data;
	my $read;
	my $retries = 0;
	my $size = $_[0];

	while (1) {

		# Try to read data from the socket
		if ($t_select->can_read ($t_timeout)) {
			
			# Read at most $size bytes
			$read = sysread ($t_client_socket, $data, $size);

			# Read error
			if (! defined ($read)) {
				error ("Read error from " . $t_client_socket->sockhost () . ": $!.");
			}
	
			# EOF
			if ($read == 0) {
				error ("Connection from " . $t_client_socket->sockhost () . " unexpectedly closed.");
			}
	
			return ($read, $data);
		}

		# Retry
		$retries++;

		# But check for error conditions first
		if ($retries > $t_retries) {
			error ("Connection from " . $t_client_socket->sockhost () . " timed out.");
		}
	}
}

################################################################################
## SUB send_data
## Write data to the client socket.
################################################################################
sub send_data {
	my $data = $_[0];
	my $retries = 0;
	my $size;
	my $total = 0;
	my $written;

	$size = length ($data);

	while (1) {

		# Try to write data to the socket
		if ($t_select->can_write ($t_timeout)) {

			$written = syswrite ($t_client_socket, $data, $size - $total, $total);

			# Write error
			if (! defined ($written)) {
				error ("Connection error from " . $t_client_socket->sockhost () . ": $!.");
			}
			
			# EOF
			if ($written == 0) {
				error ("Connection from " . $t_client_socket->sockhost () . " unexpectedly closed.");
			}
	
		}

		$total += $written;

		# Check if all data was written
		if ($total == $size) {
			return;
		}

		# Retry
		$retries++;

		# But check for error conditions first
		if ($retries > $t_retries) {
			error ("Connection from " . $t_client_socket->sockhost () . " timed out.");
		}
	}
}

################################################################################
## SUB recv_command
## Read a command from the client, ended by a new line character.
################################################################################
sub recv_command {
	my $buffer;
	my $char;
	my $command = '';
	my $read;
	my $total = 0;

	while (1) {
		
		($read, $buffer) = recv_data ($t_block_size);
		$command .= $buffer;
		$total += $read;

		# Check if the command is complete
		$char = chop ($command);
		if ($char eq "\n") {
			return $command;
		}
	
		$command .= $char;

		# Avoid overflow
		if ($total > $t_block_size) {
			error ("Received too much data from " . $t_client_socket->sockhost () . ".");
		}
	}
}

################################################################################
## SUB recv_data_block
## Read $_[0] bytes of data from the client.
################################################################################
sub recv_data_block {
	my $buffer = '';
	my $data = '';
	my $read;
	my $size = $_[0];
	my $total = 0;

	while (1) {

		($read, $buffer) = recv_data ($size - $total);
		$data .= $buffer;
		$total += $read;

		# Check if all data has been read
		if ($total == $size) {
			return $data;
		}
	}
}

################################################################################
## SUB ask_passwd
## Asks the user for a password.
################################################################################
sub ask_passwd {
	my $msg1 = $_[0];
	my $msg2 = $_[1];
	my $pwd1;
	my $pwd2;

	require Term::ReadKey;

	# Disable keyboard echo
	Term::ReadKey::ReadMode('noecho');
	
	# Promt for password
	print ($msg1);
	$pwd1 = Term::ReadKey::ReadLine(0);
	print ("\n$msg2");
	$pwd2 = Term::ReadKey::ReadLine(0);
	print ("\n");

	# Restore original settings
	Term::ReadKey::ReadMode('restore');

	if ($pwd1 ne $pwd2) {
		print ("Error: passwords do not match.\n");
		exit 1;
	}

	# Remove the trailing new line character
	chop $pwd1;

	return $pwd1;
}

################################################################################
## SUB apply_filters
## Applies filters to the given file.
################################################################################
sub apply_filters ($) {
	my ($file_name) = @_;

	foreach my $filter (@t_filters) {
		my ($regexp, $dir) = @{$filter};
		if ($file_name =~ /$regexp/) {
			print_log ("File '$file_name' matches filter '$regexp' (changing to directory '$dir')");
			return $dir . '/';
		}
	}

	return '';
}

################################################################################
## SUB install_service
## Install the Windows service.
################################################################################
sub install_service() {

	my $service_path = $0;
	my $service_params = $SERVICE_PARAMS;

	# Change the service parameter from 'install' to 'run'.
	$service_params =~ s/\-S\s+\S+/\-S run/;

	my %service_hash = (
		machine =>  '',
		name	=>  'TENTACLESRV',
		display =>  $SERVICE_NAME,
		path	=>  $service_path,
		user	=>  '',
		pwd	 =>  '',
		description => 'Tentacle Server http://sourceforge.net/projects/tentacled/',
		parameters => $service_params
	);
	
	if (Win32::Daemon::CreateService(\%service_hash)) {
		print "Successfully added.\n";
		exit 0;
	} else {
		print "Failed to add service: " . Win32::FormatMessage(Win32::Daemon::GetLastError()) . "\n";
		exit 1;
	}
}

################################################################################
## SUB uninstall_service
## Install the Windows service.
################################################################################
sub uninstall_service() {
	if (Win32::Daemon::DeleteService('', 'TENTACLESRV')) {
		print "Successfully deleted.\n";
		exit 0;
	} else {
		print "Failed to delete service: " . Win32::FormatMessage(Win32::Daemon::GetLastError()) . "\n";
		exit 1;
	}
}

################################################################################
## SUB callback_running
## Windows service callback function for the running event.
################################################################################
sub callback_running {

	if (Win32::Daemon::State() == WIN32_SERVICE_RUNNING) {
	}
}

################################################################################
## SUB callback_start
## Windows service callback function for the start event.
################################################################################
sub callback_start {

	# Accept_connections ();
	my $thr = threads->create(\&accept_connections);
	if (!defined($thr)) {
		Win32::Daemon::State(WIN32_SERVICE_STOPPED);
		Win32::Daemon::StopService();
		return;
	}
	$thr->detach();

	Win32::Daemon::State(WIN32_SERVICE_RUNNING);
}

################################################################################
## SUB callback_stop
## Windows service callback function for the stop event.
################################################################################
sub callback_stop {

	foreach my $t_server_socket (@t_server_sockets) {
		$t_server_socket->shutdown (2);
		$t_server_socket->close ();
	}

	Win32::Daemon::State(WIN32_SERVICE_STOPPED);
	Win32::Daemon::StopService();
}

################################################################################
# Main
################################################################################

# Never run as root
if ($> == 0 && $^O ne 'MSWin32') {
	print ("Error: for safety reasons $0 cannot be run with root privileges.\n");
	exit 1;
}

# Parse command line options
parse_options ();

# Check command line arguments
if ($#ARGV != -1) {
	print_help ();
	exit 1;
}

# Show IPv6 status
if ($SOCKET_MODULE eq 'IO::Socket::INET') {
	print_log ("IO::Socket::INET6 is not found. IPv6 is disabled.");
}

# Run as daemon?
if ($t_daemon == 1 && $^O ne 'MSWin32') {
	daemonize ();
}

# Handle ctr-c
if ($^O eq 'MSWin32') {
	no warnings;
	$SIG{INT2} = \&stop_server;
	use warnings;
}
else {
	$SIG{INT} = \&stop_server;
}

# Accept connections
accept_connections();

__END__

=head1 REQUIRED ARGUMENTES

=over 

=item B<< -s F<storage_directory> >>	Root directory to store the files received by the server

=back 

=head1 OPTIONS

=over

=item 	I<-a ip_address>	Address to B<listen> on (default I<0.0.0.0>).

=item	I<-c number>		B<Maximum> number of simultaneous B<connections> (default I<10>).

=item	I<-d>			Run as B<daemon>.

=item	I<-e cert>		B<OpenSSL certificate> file. Enables SSL.

=item	I<-f ca_cert>	Verify that the peer certificate is signed by a B<CA>.

=item	I<-h>			Show B<help>.

=item	I<-i>			B<Filters>.

=item	I<-k key>		B<OpenSSL private key> file.

=item	I<-m size>		B<Maximum file size> in bytes (default I<2000000b>).

=item	I<-o>			Enable file B<overwrite>.

=item	I<-p port>		B<Port to listen> on (default I<41121>).

=item	I<-q>			B<Quiet>. Do now print error messages.

=item	I<-r number>		B<Number of retries> for network opertions (default I<3>).

=item	I<-t time>		B<Time-out> for network operations in B<seconds> (default I<1s>).

=item	I<-v>			Be B<verbose>.

=item	I<-w>			Prompt for B<OpenSSL private key password>.

=item	I<-x> pwd		B<Server password>.

=back

=head1 EXIT STATUS

=over 

=item 0 on Success

=item 1 on Error

=back 

=head1 CONFIGURATION

Tentacle doesn't use any configurationf files, all the configuration is done by the options passed when it's started.

=head1 DEPENDENCIES

L<Getopt::Std>, L<IO::Select>, L<IO::Socket::INET>, L<Thread::Semaphore>, L<POSIX> 


=head1 LICENSE

This is released under the GNU Lesser General Public License.

=head1 SEE ALSO

L<Getopt::Std>, L<IO::Select>, L<IO::Socket::INET>, L<Thread::Semaphore>, L<POSIX> 

Protocol description and more info at: L<< http://openideas.info/wiki/index.php?title=Tentacle >>

=head1 COPYRIGHT

Copyright (c) 2005-2010 Artica Soluciones Tecnologicas S.L

=cut
 
