Add content-length header; increase default block size; add timings; add IP address
[ambdownload.git] / download.pl
1 #!/usr/bin/perl
2
3 use File::stat;
4 use strict;
5 use warnings;
6
7 use POSIX qw(strftime);
8 use URI::Escape;
9 use File::Copy qw( copy );
10 use File::Basename;
11 use File::stat;
12 use Digest::SHA qw(sha256_hex sha1);
13 use MIME::Base64;
14 use File::Spec;
15 use File::Spec::Functions qw(rel2abs);
16 use CGI;
17 use HTML::Entities;
18 use IO::Handle;
19 use Time::HiRes qw( usleep ualarm gettimeofday tv_interval );
20
21 my $log;
22 my $transaction="unknown";
23
24 my $logfile = "/var/log/download.log";
25 my $datadir = dirname(rel2abs($0))."/";
26 my $secretfile="/etc/apache2/download.secret";
27 my $secret;
28 my $sentheader = 0;
29 my $maxdrift = 60;
30 my $remoteip;
31 my $t0 = [gettimeofday];
32 my $filesize;
33
34 sub lprintf
35 {
36     if (defined($log))
37     {
38         my $now = strftime "%a, %d %b %Y %T %z", localtime;
39         printf $log "$now: [$$:%s][$transaction]: ", defined($remoteip)?$remoteip:"unknown";
40         printf $log @_;
41     }
42 }
43
44 sub closelog
45 {
46     close $log if defined($log);
47     $log = undef;
48 }
49
50 sub openlog
51 {
52     closelog;
53     open ($log, '>>', $logfile) || die ("Cannot open logfile $logfile: $!");
54     $log->autoflush;
55 }
56
57 sub qdie
58 {
59     my $err = shift @_;
60     my $elapsed = tv_interval ( $t0, [gettimeofday]);
61     lprintf "ERROR: $err after %.3f secs\n", $elapsed;
62     if (!$sentheader)
63     {
64         my $error = encode_entities( $err );
65         print "Status: 404 Not Found\n";
66         print "Content-type: text/html\n\n";
67         print "<html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>$error</p><hr><address>Download Server</address>";
68         print "</body></html>\n";
69     }
70     closelog;
71     die $err;
72 }
73
74 sub caughtsignal
75 {
76     my $signame = shift;
77     qdie ("Received SIG$signame");
78 }
79     
80 sub sendfile {
81     my $file = shift @_;
82     my $name = basename($file);
83
84     open my $fh, '<:raw', $file
85         or qdie "Cannot open '$file': $!";
86
87     $sentheader = 1;
88     print CGI::header(
89         -type => 'application/octet-stream',
90         -Content_length=> $filesize,
91         -attachment => $name,
92         );
93
94     binmode STDOUT, ':raw';
95
96     unless (copy $fh => \*STDOUT, 65536)
97     {
98         qdie "Cannot write to STDOUT";
99     }
100
101     close $fh
102         or qdie "Cannot close '$file': $!";
103
104     return;
105 }
106
107 sub gethash
108 {
109     return sha256_hex(shift @_);
110 }
111
112 sub getfile
113 {
114     my $fn = $datadir.(shift @_);
115     $fn = File::Spec->rel2abs( readlink($fn) ) if (-l $fn);
116     qdie ("File not found") unless ( -f $fn);
117     return $fn;
118 }
119
120 sub decodeparams
121 {
122     my $query = CGI::url(-absolute=>1);
123     my $clienttime = CGI::url_param('time');
124     my $clientid = CGI::url_param('id');
125     my $clienthash = CGI::url_param('hash');
126     my $clientfile = CGI::url_param('file');
127     $clientfile = "default" unless(defined($clientfile));
128     qdie ("Bad parameters") unless (defined($clienttime) && defined($clientid) && defined($clienthash) && ($clienttime=~/^[0-9]+$/));
129     my $now = time();
130     my $drift = $now-$clienttime;
131     qdie ("Client time has drifted - $now - $clienttime = $drift") if (($drift < -$maxdrift) || ($drift > $maxdrift));
132     qdie ("Bad ID") unless ($clientid=~/^[-+._\@a-zA-Z0-9]+$/);
133     qdie ("Bad filename") unless ($clientfile=~/^[-+._a-zA-Z0-9]+$/);
134     qdie ("Bad filename") if ($clientfile=~/^\./);
135
136     my $hash = gethash($clienttime.":".$clientid.":".$clientfile.":".$secret);
137     qdie ("Bad hash") unless ($hash eq $clienthash);
138     my $fn = getfile($clientfile);
139     $clientfile = basename ($fn);
140     $transaction=$hash." ".$clientfile." ".$clientid;
141     return $fn;
142 }
143
144 sub doinfo
145 {
146     my $clientfile = shift @_;
147     my $fn = getfile($clientfile);
148     $clientfile = basename ($fn);
149     my $size = "unknown";
150     my $sb = stat($fn);
151     $size = $sb->size if (defined($sb) && defined($sb->size));
152     my $md5sum = "unknown";
153     my $md5fn = $fn.".md5sum";
154     if ( -r $md5fn )
155     {
156         my $md5;
157         open $md5, "<", $md5fn || qdie ("Can't read md5sum");
158         while (<$md5>)
159         {
160             chomp;
161             $md5sum = $1 if (/^([a-f0-9]+)\b/);
162         }
163         close $md5;
164     }
165     $sentheader = 1;
166     print CGI::header(
167         -type => 'text/plain' );
168
169     print "$clientfile $size $md5sum\n";
170 }
171
172 open (my $sfh, "<", $secretfile) || qdie("Can't open secret file $secretfile: $!");
173 chomp($secret=join("",<$sfh>));
174 close ($sfh);
175
176 if (!defined($ENV{DOCUMENT_ROOT}) && !defined($ENV{SERVER_NAME}))
177 {
178     die ("Bad parameters") unless ($#ARGV == 1);
179     my $t = time();
180     printf "id=%s&file=%s&time=%s&hash=%s",uri_escape($ARGV[0]),uri_escape($ARGV[1]),$t,gethash($t.":".$ARGV[0].":".$ARGV[1].":".$secret)."\n";
181     exit(0);
182 }
183 else
184 {
185     my $info = CGI::url_param('info');
186     if (defined($info))
187     {
188         doinfo($info);
189         exit 0;
190     }
191     $remoteip = CGI::remote_addr();
192
193     openlog;
194     my $file = decodeparams;
195     my $sb = stat($file);
196     $filesize = $sb->size;
197     lprintf("STARTING\n");
198     $SIG{INT} = \&caughtsignal;
199     $SIG{QUIT} = \&caughtsignal;
200     $SIG{PIPE} = \&caughtsignal;
201     $SIG{HUP} = \&caughtsignal;
202     $SIG{KILL} = \&caughtsignal;
203     $SIG{TERM} = \&caughtsignal;
204     sendfile($file);
205     my $elapsed = tv_interval ( $t0, [gettimeofday]);
206     my $rate = $filesize/(1000000.0*(($elapsed<0.001)?0.001:$elapsed));
207     lprintf("SUCCESS %d bytes %.3f MB/s %.3f secs\n", $filesize, $rate, $elapsed);
208     closelog;
209
210     exit(0);
211 }