#!/usr/bin/perl # FREP_FileServer - file server used by FREP. # Calculate file signatures as well as sending files (uses sendfile() for zero-copy # file transfers. # # Copyright (C) 2007 Mark Steele http://www.control-alt-del.org/code # # 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; either version 2 # of the License, or (at your option) any later version. # # 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. # use Event::Lib; use POSIX qw/SIGHUP SIGTERM/; use IO::Socket::INET; use Digest::MD5; use Sys::Syscall qw(sendfile); use Number::Bytes::Human qw(format_bytes); use BerkeleyDB; use MLDBM qw(BerkeleyDB::Hash Storable) ; use Fcntl; use XML::Simple; use strict; use Sys::Syslog; $| = 1; my %stats; my $CONFIG; eval {$CONFIG = XMLin("/etc/FMon.conf",ForceArray => ['Dir','Pool']);}; die "Couldn't parse config: $!" if ($@); ## Configuration sanity checks for (qw(Port Address ValidPathRegex ConnectionTimeout SignatureCache)) { die("Missing configuration parameter: $_") if (!$CONFIG->{FileServer}->{$_}); } ## End sanity checking # Create a listening socket my $server = IO::Socket::INET->new(LocalAddr => $CONFIG->{FileServer}->{Address}, LocalPort => $CONFIG->{FileServer}->{Port}, Proto => 'tcp', ReuseAddr => SO_REUSEADDR, Listen => 1, Blocking => 0,) or die $!; my %filecache; tie %filecache, 'MLDBM', -Filename => $CONFIG->{FileServer}->{SignatureCache}, -Flags=> DB_CREATE or die "Cannot open file: $!\n" ; my $main = event_new($server, EV_READ|EV_PERSIST, \&handle_incoming); $main->add; my $signal_hup = signal_new(SIGHUP, \&sig_hup_stats); $signal_hup->add; my $signal_term = signal_new(SIGTERM, \&sig_term); $signal_term->add; my $timer_stats = timer_new(\&sig_hup_stats); $timer_stats->add(5); $0 = "FREP_FileServer:"; event_mainloop(); sub handle_incoming { my $e = shift; my $h = $e->fh; my $client = $h->accept or return; $client->blocking(0); my $event = event_new($client, EV_READ, \&handle_client); $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); } sub handle_client { my $e = shift; my $h = $e->fh; my $type = shift; if ($type == EV_TIMEOUT) { $e->remove; undef($e); close($h); $stats{"errors"}++; return; } my $cmd = shift; sysread($h,my $buf, 8192); if (!$buf) { close($h); $e->remove; undef($e); $stats{"errors"}++; return; } $cmd .= $buf; if (length($cmd) > 8192) { close($h); $e->remove; undef($e); $stats{"errors"}++; return; } if ($cmd !~ /(.*)\r?\n/) { my $event = event_new($h, EV_READ, \&handle_client,$cmd); $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); return; } my $file = $1; $file =~ s/(?:\r|\n)//g; if ($file !~ /^(GET|SIG) /) { $stats{"errors"}++; close($h); $e->remove; undef($e); return; } my $cmd = $1; $file =~ s/^(?:GET|SIG) //; if ($file !~ /$CONFIG->{FileServer}->{ValidPathRegex}/) { $stats{"errors"}++; close($h); $e->remove; undef($e); return; } my ($filesize,$mtime) = (stat($file))[7,9]; if (!-f $file || -d _ || -z _) { $stats{"errors"}++; close($h); $e->remove; undef($e); return; } if ($cmd eq 'GET') { my $event = event_new($h, EV_WRITE, \&send_file, $file, 0,$filesize,open_file($file)); if ($event) { $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); } else { close($h); $e->remove; undef($e); return; } } elsif ($cmd eq 'SIG') { my $sig; debug_log("SIG requested for $file"); if (my $cdata = $filecache{$file}) { debug_log("retrieved cached data"); if ($cdata->{MTIME} == $mtime) { debug_log("cached data matches on mtime"); $stats{'cachehit'}++; $sig = $cdata->{SIG} . '|' . $cdata->{FSIZE} . '|' . $cdata->{MTIME}; my $event = event_new($h, EV_WRITE, \&send_sig, $sig,0); $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); return; } else { $stats{'cachemiss'}++; if (my $md5 = md5_file($file)) { my $cdata = { SIG => $md5, FSIZE => $filesize, MTIME => $mtime}; $sig = $cdata->{SIG} . '|' . $cdata->{FSIZE} . '|' . $cdata->{MTIME}; $filecache{$file} = $cdata; my $event = event_new($h, EV_WRITE, \&send_sig, $sig,0); $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); return; } else { ## Barf debug_log("couldn't get MD5 checksum to add to cache. Barfing."); $stats{"errors"}++; close($h); $e->remove; undef($e); return; } } } else { debug_log("cache miss"); $stats{'cachemiss'}++; if (my $md5 = md5_file($file)) { my $cdata = { SIG => $md5, FSIZE => $filesize, MTIME => $mtime}; $sig = $cdata->{SIG} . '|' . $cdata->{FSIZE} . '|' . $cdata->{MTIME}; $filecache{$file} = $cdata; my $event = event_new($h, EV_WRITE, \&send_sig, $sig,0); $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); return; } else { ## Barf debug_log("couldn't get MD5 checksum to add to cache. Barfing."); $stats{"errors"}++; close($h); $e->remove; undef($e); return; } } } else { $stats{"errors"}++; close($h); $e->remove; undef($e); return; } } sub send_file { my $e = shift; my $type = shift; my $h = $e->fh; my $file = shift; my $offset = shift; my $filesize = shift; my $fh = shift; if ($type == EV_TIMEOUT || !$file || !$fh) { $stats{"errors"}++; close($h); $e->remove; undef $e; close($fh); return; } my $sent = sendfile(fileno($h),fileno($fh),$filesize); if (!$sent) { ## Socket was ready for writing but couldn't write any data $stats{"errors"}++; close($h); close($fh); $e->remove; undef $e; return; } $stats{"bytes"} += $sent; if (($sent+$offset) == $filesize) { $stats{"sent"}++; close($h); close($fh); $e->remove; undef($e); } else { my $event = event_new($h, EV_WRITE, \&send_file, $file, $sent+$offset,$filesize,$fh); $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); } } sub md5_file { my $file = shift; if (!open(FILE,$file)) { return undef; } binmode(FILE); my $digest = Digest::MD5->new->addfile(*FILE)->hexdigest; close(FILE); return $digest; } sub open_file { my $file = shift; local *F; if (!sysopen(F,$file,O_RDONLY)) { return undef; } return *F; } sub sig_hup_stats { my $e = shift; my $type = shift; if ($type == EV_TIMEOUT) { $e->add($CONFIG->{FileServer}->{ConnectionTimeout}); } $0 = qq#FREP_FileServer => chits: $stats{cachehit} cmiss: $stats{cachemiss} err: $stats{errors} files: $stats{'sent'} data: # . format_bytes($stats{'bytes'}); } sub send_sig { my $e = shift; my $h = $e->fh; my $type = shift; my $signature = shift; my $offset = shift; if ($type == EV_TIMEOUT) { $e->remove; undef($e); close($h); $stats{"errors"}++; return; } my $ret = 0; eval { $ret = syswrite($h,$signature,512,$offset); }; if ($@) { $e->remove; undef($e); close($h); return; } if ($ret == 0) { ## ERROR: couldn't write anything debug_log("couldn't write whole signature to client"); $e->remove; undef($e); close($h); return; } if ($ret != (length($signature)-$offset)) { ## Couldn't write everything, reschedule my $event = event_new($h, EV_WRITE, \&send_sig, $signature,$offset+$ret); $event->add($CONFIG->{FileServer}->{ConnectionTimeout}); } else { $e->remove; undef($e); close($h); } } sub sig_term { untie(%filecache); exit; } sub debug_log { return if (!$CONFIG->{FileServer}->{Debug}); openlog("FMon_FileServer", 'cons,pid', 'user'); syslog('debug', '%s', shift); closelog(); }