#!/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 # 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.4.0 =head1 USAGE tentacle_server B<< -s F >> [I] =head1 DESCRIPTION B is a server for B, a B 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) are designed to be run from the command line or called from a shell script, and B. =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"; # Program version our $VERSION = '0.4.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; my $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; #Bridge IP to actuate as a proxy my $t_proxy_ip = undef; # Proxy port by default 41121 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 [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\n"); print ("\t-b proxy_ip_address\t\tProxied server address.\n\n"); print ("\t-g proxy_port\t\tPort of proxied server.\n\n"); print ("\t-T\t\tEnable tcpwrappers support.\n"); print ("\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; } # 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 'install') { install_service(); } elsif ($service_action eq 'uninstall') { uninstall_service(); } elsif ($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 server socket. ################################################################################ sub open_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 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: $!."); } $select = IO::Select->new(); foreach my $t_server_socket (@t_server_sockets){ $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->close (); print_log ("Proxy socket closed"); } ################################################################################ ## SUB stop_server ## Close the server socket. ################################################################################ sub stop_server { foreach my $t_server_socket (@t_server_sockets) { $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; # Start server start_server (); # Initialize semaphore $t_sem = Thread::Semaphore->new ($t_max_conn); while (1) { my @ready = $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->sockhost ()); # 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(); } usleep (1000); } } ################################################################################ ## SUB serve_client ## Serve a connected client. ################################################################################ sub serve_client() { if ($t_use_libwrap) { if (! hosts_ctl($t_program_name, $t_client_socket)) { print_log ("Connection from " . $t_client_socket->sockhost() . " is closed by tcpwrappers."); $t_client_socket->close (); $t_sem->up(); return; } } 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->close (); $t_sem->up(); } ################################################################################ ## SUB serve_proxy_connection ## Actuate as a proxy between its client and other tentacle server. ################################################################################ sub serve_proxy_connection { my $read; my $data=1; # Start a connection with the other Tentacle Server open_proxy(); my $command; # Read commands while ($command = recv_command ($t_block_size)) { # Client wants to send a file if ($command =~ /^SEND <(.*)> SIZE (\d+)$/) { recv_file_proxy($command, $2); } # Client wants to receive a file elsif ($command =~ /^RECV <(.*)>$/) { send_file_proxy ($command); } # Quit elsif ($command =~ /^QUIT$/) { print_log ("Connection closed from " . $t_client_socket->sockhost ()); # End proxy connection send_data_proxy("QUIT\n"); last; } # Unknown command else { print_log ("Unknown command '$command' from " . $t_client_socket->sockhost ()); last; } } # End a connection with the other 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_proxy ## Redirect file from agent to proxy ################################################################################ sub recv_file_proxy ($$) { my ($command, $size) = @_; # Send command to proxy print_log ("[PROXY] Host: ".$t_client_socket->sockhost()." send ".$command." to ".$t_proxy_socket->sockhost ()); send_data_proxy($command."\n"); # Proxied server response my $rc = dump_data($t_proxy_socket, $t_client_socket); # Check if there was an error if ($rc == -1) { return; } # Client send data to server $rc = dump_data($t_client_socket, $t_proxy_socket, $size); # Check if there was an error if ($rc == -1) { return; } # Server says if data was recieved or not $rc = dump_data($t_proxy_socket, $t_client_socket); # Check if there was an error if ($rc == -1) { return; } } ################################################################################ ## 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_proxy ## Redirect file from agent to proxy ################################################################################ sub send_file_proxy ($) { my ($command) = @_; my $size; # Send command to proxy print_log ("[PROXY] ".$t_client_socket->sockhost()." send ".$command." to ".$t_proxy_socket->sockhost ()); send_data_proxy($command."\n"); $command = recv_command_proxy($t_block_size); if ($command =~ /^RECV SIZE (\d+)$/) { $size = $1; } print_log ("[PROXY] ".$t_proxy_socket->sockhost()." send ".$command." to ".$t_client_socket->sockhost ()); send_data($command."\n"); # Client send OK to server my $rc = dump_data($t_client_socket, $t_proxy_socket); # Check if there was an error if ($rc == -1) { return; } # Proxied server send the file to client $rc = dump_data($t_proxy_socket, $t_client_socket, $size); # Check if there was an error if ($rc == -1) { return; } } ################################################################################ ## 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 = ) { 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 dump_data ## Dump data from a socket to another one. Following Tentacle Protocol. ################################################################################ sub dump_data($$;$) { my ($from, $to, $size) = @_; my $read; my $data; my $buffer = ""; my $written; my $total = $size; my $t_select_read = undef; my $t_select_write = undef; # Assign the correct selector for each socket if ($from == $t_proxy_socket) { # We must read from t_proxy_socket $t_select_read = $t_proxy_select; $t_select_write = $t_select; } else { # We must read from t_client_socket $t_select_read = $t_select; $t_select_write = $t_proxy_select; } while (1) { # Ensure we can read from socket if ($t_select_read->can_read()) { # Ensure we can write from socket if ($t_select_write->can_write()) { $read = ""; $read = sysread ($from, $data, $t_block_size); # Read error if (! defined ($read)) { error ("Read error from " . $from->sockhost () . ": $!."); return -1; } # EOF if ($read == 0) { error ("Connection from " . $from->sockhost () . " unexpectedly closed."); return -1; } $written = syswrite ($to, $data, $read); if ($written != $read) { error ("Connection from " . $to->sockhost () . " unexpectedly closed."); return -1; } $buffer .= $data; if (! defined ($size)) { # If no size defined check for \n # because a command was sent. if ($buffer =~ /\n$/) { # Delete CHAR \n chop($buffer); print_log ("[PROXY] ".$from->sockhost()." send ".$buffer." to ".$to->sockhost ()); # Returns error if proxy returns error to end connection if ($buffer =~ /SEND ERR/) { return -1; } else { return 0; } } } else { # If size is defined check if all bytes were sent $size = $size - $read; if ($size == 0) { print_log ("[PROXY] ".$from->sockhost()." send ".$total."b to ".$to->sockhost()); return 0; } } } } } } ################################################################################ ## SUB recv_command_proxy ## Read a command from the proxied server, ended by a new line character. ################################################################################ sub recv_command_proxy { my $buffer; my $char; my $command = ''; my $read; my $total = 0; while (1) { ($read, $buffer) = recv_data_proxy ($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_proxy_socket->sockhost () . "."); } } } ################################################################################ ## 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 = "-s \"$t_directory\" -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->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 >> Root directory to store the files received by the server =back =head1 OPTIONS =over =item I<-a ip_address> Address to B on (default I<0.0.0.0>). =item I<-c number> B number of simultaneous B (default I<10>). =item I<-d> Run as B. =item I<-e cert> B file. Enables SSL. =item I<-f ca_cert> Verify that the peer certificate is signed by a B. =item I<-h> Show B. =item I<-i> B. =item I<-k key> B file. =item I<-m size> B in bytes (default I<2000000b>). =item I<-o> Enable file B. =item I<-p port> B on (default I<41121>). =item I<-q> B. Do now print error messages. =item I<-r number> B for network opertions (default I<3>). =item I<-t time> B for network operations in B (default I<1s>). =item I<-v> Be B. =item I<-w> Prompt for B. =item I<-x> pwd B. =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, L, L, L, L =head1 LICENSE This is released under the GNU Lesser General Public License. =head1 SEE ALSO L, L, L, L, L 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