#!/usr/bin/perl use Net::Gnutella; use Data::Dumper; use IO::File; use strict; use vars qw/$gnutella $shared_timestamp $shared_size @shared_files %shared_words %common_words %current_downloads %current_connects/; use constant SERVER_ADDRESS => "198.142.4.65"; use constant SERVER_PORT => 6348; use constant SERVER_SPEED => 128000; use constant SHARED_FILE_LIST => "list.txt"; use constant MAX_CONNECTS => 2; use constant MAX_DOWNLOADS => 6; use constant MAX_DOWNLOADS_PER_IP => 2; printf STDERR "%s: Starting server\n", scalar localtime; $gnutella = new Net::Gnutella; my $server = $gnutella->new_server( Server => SERVER_ADDRESS, Port => SERVER_PORT, Allow => GNUTELLA_CONNECT|GNUTELLA_REQUEST, Debug => 2, ); $gnutella->add_handler("connected", \&on_connect); $gnutella->add_handler("disconnect", \&on_disconnect); $gnutella->add_handler("ping", \&on_ping); $gnutella->add_handler("query", \&on_query); $gnutella->add_handler("download_req", \&on_download_req); $gnutella->add_handler("download_complete", \&on_download_end); $gnutella->add_handler("download_error", \&on_download_end); load_shared_files(); $gnutella->schedule("1m", \&send_ping); $gnutella->schedule("1m", \&load_shared_files); $gnutella->start; # One level keepalive ping # sub send_ping { my $ping = Net::Gnutella::Packet::Ping->new( Msgid => $gnutella->id, TTL => 1, ); foreach my $conn ($gnutella->connections) { $conn->send_packet($ping); } $gnutella->schedule("1m", \&send_ping); } sub on_connect { my ($self) = @_; my $ip = $self->ip; unless (allowed_to_connect($ip)) { $self->disconnect; return; } $current_connects{ $ip }++; printf STDERR "%s: Connection from %s\n", scalar localtime, $ip; my $ping = Net::Gnutella::Packet::Ping->new( Msgid => $gnutella->id, ); $self->send_packet($ping); } sub on_disconnect { my ($self) = @_; my $ip = $self->ip; printf STDERR "%s: Disconnect from %s\n", scalar localtime, $ip; $current_connects{ $ip }--; } sub on_ping { my ($self, $event) = @_; my $pong = Net::Gnutella::Packet::Pong->new( Msgid => $event->packet->msgid, Ip => SERVER_ADDRESS, Port => SERVER_PORT, Count => scalar(@shared_files), Size => int($shared_size / 1024), ); $self->send_packet($pong); } sub on_query { my ($self, $event) = @_; my $packet = $event->packet; my @tokens = tokenise($packet->query); unless (scalar @tokens) { return; } my $results = find_by_tokens(@tokens); unless (defined $results) { return; } my $reply = Net::Gnutella::Packet::Reply->new( Msgid => $packet->msgid, Ip => SERVER_ADDRESS, Port => SERVER_PORT, Speed => SERVER_SPEED, Results => $results, Guid => $gnutella->id, ); $self->send_packet($reply); } sub on_download_req { my ($self, $event) = @_; my $request = $event->packet; if ($request->method eq 'GET' and $request->url->path =~ m|/get/(\d+)/(.+)|) { my ($index, $file) = ($1, $2); if (my $filename = find_by_index($index, $file)) { my $ip = $self->ip; if (allowed_to_download($ip)) { my $offset = $1 if $request->headers->header("Range") =~ /^bytes=(\d+)-$/; if ($self->send_file($filename, $offset)) { printf STDERR "%s: Uploading file '%s' to %s\n", scalar localtime, $file, $ip; $current_downloads{ $ip }++; } } else { $self->send_error(503); # SERVICE UNAVAILABLE } } else { print "not found [$index] [$file]\n"; $self->send_error(404); # NOT FOUND } } elsif ($request->method eq 'GET' and $request->url->path =~ m|/admin(?:/(.*))?|) { my $page = $1 || "menu.html"; if ($page eq "menu.html") { $self->send_page(<Gnutella menu
  • List all shared files (by keyword)
  • List all connections
  • List host cache EOPAGE } elsif ($page eq "shared.html") { $self->send_page(Dumper(\%shared_words)); } elsif ($page eq "connections.html") { $self->send_page("
    ".Dumper(\%current_connects, \%current_downloads)."
    "); } elsif ($page eq "hosts.html") { $self->send_page("
    ".Dumper($gnutella->{_host_cache})."
    "); } else { $self->send_error(404); # NOT FOUND } } else { $self->send_error(403); # FORBIDDEN } } # Decrement counters # sub on_download_end { my ($self) = @_; my $ip = $self->ip; printf STDERR "%s: Upload complete to '%s'\n", scalar localtime, $ip; $current_downloads{ $ip }--; } sub allowed_to_connect { my $ip = shift; my $count = 0; $count += $current_connects{$_} foreach keys %current_connects; return 1 if $ip =~ /^198\.142\.4\.6[567]$/; return if $count >= MAX_CONNECTS; return 1; } # Returns true (1) if download permitted # sub allowed_to_download { my $ip = shift; my $count = 0; $count += $current_downloads{$_} foreach keys %current_downloads; return 1 if $ip =~ /^198\.142\.4\.6[567]$/; return if $count >= MAX_DOWNLOADS; return if $current_downloads{ $ip } >= MAX_DOWNLOADS_PER_IP; return 1; } # Returns full path if the filename shared at index matches... # sub find_by_index { my ($index, $filename) = @_; my $full = $shared_files[$index]->[0]; return unless length $full; return unless -f $full; my $file = (File::Spec->splitpath($full))[2]; return $full if $file eq $filename; return; } # Returns filenames which are in the lists for the union of tokens # sub find_by_tokens { my @tokens = @_; my %count; my @ret; foreach my $token (@tokens) { $count{$_}++ foreach @{ $shared_words{$token} || [] } } foreach my $index (grep { $count{$_} == scalar(@tokens) } keys %count) { my $full = $shared_files[$index]->[0]; my $size = $shared_files[$index]->[1]; my $file = (File::Spec->splitpath($full))[2]; push @ret, [ $index, $size, $file ]; } return unless scalar @ret; return \@ret; } # Loads shared file list if newer than previous version # sub load_shared_files { my $new_timestamp = (stat(SHARED_FILE_LIST))[9]; if ($shared_timestamp == $new_timestamp) { $gnutella->schedule("1m", \&load_shared_files); return; } printf STDERR "%s: Reading list of shared files (%d != %d)\n", scalar localtime, $shared_timestamp, $new_timestamp; @shared_files = (); my $fh = new IO::File SHARED_FILE_LIST, O_RDONLY; push @shared_files, [ split /\t/ ] foreach grep { chomp; not /^#/ } <$fh>; undef $fh; printf STDERR "%s: Building token list\n", scalar localtime; build_token_list(); printf STDERR "%s: Done\n", scalar localtime; $shared_timestamp = $new_timestamp; $gnutella->schedule("1m", \&load_shared_files); } # Builds hash of shared files # sub build_token_list { %shared_words = (); $shared_size = 0; for (my $i = 0; $i <= $#shared_files; $i++) { my $file = $shared_files[$i]->[0]; my $size = $shared_files[$i]->[1] ||= -s $file; $shared_size += $size; foreach (tokenise($file)) { push @{$shared_words{$_}}, $i; if (scalar @{$shared_words{$_}} > 256) { delete $shared_words{$_}; $common_words{$_} = 1; } } } } # Return an array containing all alpha strings of more # than two characters length (all elements are lowercase) # # 10,000/sec on 333Mhz Celeron # sub tokenise { my $line = lc shift; my %seen; $line =~ s/[^a-z]+/ /g; foreach (split(" ", $line)) { next if length $_ < 3; $seen{$_} = 1 unless exists $common_words{$_}; } return keys %seen; }