24 use Pod::Usage; |
24 use Pod::Usage; |
25 use File::Basename; |
25 use File::Basename; |
26 use POSIX qw(:sys_wait_h setsid); |
26 use POSIX qw(:sys_wait_h setsid); |
27 use Cwd; |
27 use Cwd; |
28 |
28 |
|
29 my $ME = basename $0; |
|
30 |
29 my $opt_addr = "0.0.0.0"; |
31 my $opt_addr = "0.0.0.0"; |
|
32 my $opt_auth = 1; |
30 my $opt_port = 8080; |
33 my $opt_port = 8080; |
31 my $opt_lines = 10; |
34 my $opt_lines = 10; |
32 my $opt_daemon = 1; |
35 my $opt_daemon = 1; |
33 my $opt_kill = 0; |
36 my $opt_kill = 0; |
34 my $opt_debug = 0; |
37 my $opt_debug = 0; |
35 my $opt_htpasswd = "htpasswd"; |
38 my $opt_htpasswd = "htpasswd"; |
|
39 my $opt_realm = $ME; |
36 my $opt_ssl = 1; |
40 my $opt_ssl = 1; |
37 my $opt_ssl_cert = "crt.pem"; |
41 my $opt_ssl_cert = "crt.pem"; |
38 my $opt_ssl_key = "key.pem"; |
42 my $opt_ssl_key = "key.pem"; |
39 |
|
40 my $ME = basename $0; |
|
41 |
43 |
42 # these vars will be filled with the real dirs later |
44 # these vars will be filled with the real dirs later |
43 my $rundir = [ "/var/run/$ME", "$ENV{HOME}/.$ME" ]; |
45 my $rundir = [ "/var/run/$ME", "$ENV{HOME}/.$ME" ]; |
44 my $logdir = [ "/var/log/$ME", "$ENV{HOME}/.$ME" ]; |
46 my $logdir = [ "/var/log/$ME", "$ENV{HOME}/.$ME" ]; |
45 |
47 |
54 # remember the pid that is actually written to the pid file so we can ensure |
56 # remember the pid that is actually written to the pid file so we can ensure |
55 # that only the process with that pid is attempting to remove the pidfile at |
57 # that only the process with that pid is attempting to remove the pidfile at |
56 # exit |
58 # exit |
57 my $masterpid; |
59 my $masterpid; |
58 |
60 |
|
61 # usernames & password hashes |
|
62 my $authdata; |
59 |
63 |
60 # attempt to generalize some interface differences between |
64 # attempt to generalize some interface differences between |
61 # IO::Socket::{INET,SSL}; currently we need to pass an SSL specific argument |
65 # IO::Socket::{INET,SSL}; currently we need to pass an SSL specific argument |
62 # when closing an SSL Socket to avoid affecting the socket in |
66 # when closing an SSL Socket to avoid affecting the socket in |
63 # parent(s)/children; passing unknown arguments to the 'close' method of non |
67 # parent(s)/children; passing unknown arguments to the 'close' method of non |
141 my $self = shift; |
145 my $self = shift; |
142 return $ISA[0] eq "IO::Socket::SSL"; |
146 return $ISA[0] eq "IO::Socket::SSL"; |
143 |
147 |
144 } |
148 } |
145 |
149 |
|
150 # authentication |
|
151 package Authen::hlog; |
|
152 |
|
153 use Crypt::PasswdMD5; |
|
154 use Digest::SHA1 qw(sha1_base64); |
|
155 use MIME::Base64 qw(decode_base64); |
|
156 |
|
157 sub new { |
|
158 |
|
159 my $class = shift; |
|
160 |
|
161 my $self = { @_ }; |
|
162 |
|
163 die "At least one of 'filename' or 'authdata' parameters is required" unless $self->{filename} || $self->{authdata}; |
|
164 |
|
165 bless $self, $class; |
|
166 $self->authdata if $self->{filename}; |
|
167 |
|
168 return $self; |
|
169 |
|
170 } |
|
171 |
|
172 sub verify_base64 { |
|
173 |
|
174 my $self = shift; |
|
175 return $self->verify(split /:/, decode_base64($_[0])); |
|
176 |
|
177 } |
|
178 |
|
179 |
|
180 sub verify { |
|
181 |
|
182 my $self = shift; |
|
183 |
|
184 my ($u, $p) = @_; |
|
185 |
|
186 my $hp = $self->{authdata}->{$u}; |
|
187 |
|
188 # crypt? |
|
189 if (length $hp == 13) { |
|
190 return crypt ($p, $hp) eq $hp; |
|
191 # apache md5? |
|
192 } elsif (length $hp == 37 && $hp =~ /^\$apr/) { |
|
193 return apache_md5_crypt ($p, $hp) eq $hp; |
|
194 } elsif ($hp =~ s/^\{SHA\}//) { |
|
195 # remove trailing equality signs because sha1_base64 refuses to add any |
|
196 $hp =~ s/=*$//; |
|
197 return sha1_base64($p) eq $hp; |
|
198 } else { |
|
199 warn "unknown hash format: >>>$hp<<<"; |
|
200 } |
|
201 |
|
202 return; |
|
203 |
|
204 } |
|
205 |
|
206 sub authdata { |
|
207 |
|
208 my $self = shift; |
|
209 |
|
210 my ($htpasswd) = @_ || $self->{filename} || die "Missing filename"; |
|
211 |
|
212 $self->{authdata} = {}; |
|
213 |
|
214 open H, "<$htpasswd" or die "Cant open '<$htpasswd': $!"; |
|
215 while (my $line = <H>) { |
|
216 |
|
217 chomp $line; |
|
218 # htpasswd lines may have more than 2 fields |
|
219 my ($u, $p) = split /:/, $line, 3; |
|
220 |
|
221 unless ($u && $p) { |
|
222 warn "invalid htpasswd line in '$htpasswd' at line $."; |
|
223 next; |
|
224 } |
|
225 |
|
226 warn "duplicate user '$u' in '$htpasswd' at line $. - overriding previous record" if exists $self->{authdata}->{$u}; |
|
227 $self->{authdata}->{$u} = $p; |
|
228 |
|
229 } |
|
230 |
|
231 close H or warn "Cant close '<$htpasswd': $!"; |
|
232 |
|
233 warn "no authentication data found" unless %{$self->{authdata}}; |
|
234 |
|
235 return $self->{authdata}; |
|
236 |
|
237 } |
|
238 |
|
239 # back to main package |
146 package main; |
240 package main; |
147 |
241 |
148 END { |
242 END { |
149 unlink $pidfile |
243 unlink $pidfile |
150 if defined $pidfile and not ref $pidfile and defined $masterpid and $masterpid == $$; |
244 if defined $pidfile and not ref $pidfile and defined $masterpid and $masterpid == $$; |
165 |
259 |
166 MAIN: { |
260 MAIN: { |
167 |
261 |
168 GetOptions( |
262 GetOptions( |
169 "addr=s" => \$opt_addr, |
263 "addr=s" => \$opt_addr, |
|
264 "auth!" => \$opt_auth, |
170 "port=i" => \$opt_port, |
265 "port=i" => \$opt_port, |
171 "lines=i" => \$opt_lines, |
266 "lines=i" => \$opt_lines, |
172 "daemon!" => \$opt_daemon, |
267 "daemon!" => \$opt_daemon, |
173 "debug!" => \$opt_debug, |
268 "debug!" => \$opt_debug, |
174 "kill" => \$opt_kill, |
269 "kill" => \$opt_kill, |
175 "help" => sub { pod2usage(-verbose => 1, -exitval => 0) }, |
270 "help" => sub { pod2usage(-verbose => 1, -exitval => 0) }, |
176 "man" => sub { pod2usage(-verbose => 2, -exitval => 0) }, |
271 "man" => sub { pod2usage(-verbose => 2, -exitval => 0) }, |
177 "htpasswd=s" => \$opt_htpasswd, |
272 "htpasswd=s" => \$opt_htpasswd, |
|
273 "realm=s" => \$opt_realm, |
178 "ssl!" => \$opt_ssl, |
274 "ssl!" => \$opt_ssl, |
179 "ssl-cert=s" => \$opt_ssl_cert, |
275 "ssl-cert=s" => \$opt_ssl_cert, |
180 "ssl-key=s" => \$opt_ssl_key |
276 "ssl-key=s" => \$opt_ssl_key |
181 ) or pod2usage(); |
277 ) or pod2usage(); |
182 |
278 |
183 $IO::Socket::hlog::DEBUG = $opt_debug; |
279 $IO::Socket::hlog::DEBUG = $opt_debug; |
|
280 |
|
281 $authdata = new Authen::hlog(filename => $opt_htpasswd, realm => $opt_realm) if $opt_auth; |
184 |
282 |
185 if (defined($logdir = find_writable_dir(@$logdir))) { |
283 if (defined($logdir = find_writable_dir(@$logdir))) { |
186 $access = sprintf $$access, $logdir; |
284 $access = sprintf $$access, $logdir; |
187 $errors = sprintf $$errors, $logdir; |
285 $errors = sprintf $$errors, $logdir; |
188 log_open($access); |
286 log_open($access); |
361 |
459 |
362 # number of lines and tag to show |
460 # number of lines and tag to show |
363 my $lines = (s/(\d+)$// ? $1 : $opt_lines); |
461 my $lines = (s/(\d+)$// ? $1 : $opt_lines); |
364 my $tag = (s/^\/*(\w+)// ? $1 : "default"); |
462 my $tag = (s/^\/*(\w+)// ? $1 : "default"); |
365 |
463 |
366 # read the header(s) and discard |
464 my $authorized; |
367 while (<$client>) { last if /^\s*$/ } |
465 $authorized = 1 unless $opt_auth; |
|
466 # read and verify (first) authentication header and discard any other headers |
|
467 while (<$client>) { |
|
468 |
|
469 if (!defined $authorized && /^Authorization: Basic ([[:alnum:]+\/=]+)\r?$/) { |
|
470 $authorized = $authdata->verify_base64($1); |
|
471 log_write("authentication failure from " . $client->peerhost) unless $authorized; |
|
472 } |
|
473 last if /^\s*$/; |
|
474 |
|
475 } |
|
476 |
|
477 unless ($authorized) { |
|
478 |
|
479 $client->print(http { |
|
480 code => "401 Unauthorized", |
|
481 headers => { "WWW-Authenticate" => "Basic realm=\"$authdata->{realm}\"" } |
|
482 }, "not authorized"); |
|
483 return; |
|
484 |
|
485 } |
368 |
486 |
369 if (not exists $FILE{$tag}) { |
487 if (not exists $FILE{$tag}) { |
370 $client->print(http "500 unknown file tag", |
488 $client->print(http "500 unknown file tag", |
371 "Sorry, unknown file tag \"$tag\""); |
489 "Sorry, unknown file tag \"$tag\""); |
372 log_write("unknown tag $tag"); |
490 log_write("unknown tag $tag"); |
426 seek($r{fh}, 0, 0); |
544 seek($r{fh}, 0, 0); |
427 return %r; |
545 return %r; |
428 } |
546 } |
429 |
547 |
430 sub http($@) { |
548 sub http($@) { |
431 my $code = shift; |
549 |
432 my $date = date1123(); |
550 my ($headers, $code, $date) = (''); |
|
551 |
|
552 if (ref $_[0] eq "HASH") { |
|
553 |
|
554 my $h; |
|
555 ($code, $date, $h) = @{$_[0]}{'code', 'date', 'headers'}; |
|
556 $headers = ( join "\n", map { "$_: $h->{$_}" } keys %{$h} ) . "\n" if defined $h; |
|
557 shift; |
|
558 |
|
559 } else { |
|
560 |
|
561 $code = shift; |
|
562 |
|
563 } |
|
564 |
|
565 $date ||= date1123(); |
433 |
566 |
434 my $type = $_[0] =~ /^<!DOCTYPE HTML/ ? "text/html" : "text/plain"; |
567 my $type = $_[0] =~ /^<!DOCTYPE HTML/ ? "text/html" : "text/plain"; |
435 |
568 |
436 return <<__EOF, @_; |
569 return <<__EOF, @_; |
437 HTTP/1.1 $code |
570 HTTP/1.1 $code |
438 Date: $date |
571 Date: $date |
439 Connection: close |
572 Connection: close |
440 Content-Type: $type |
573 Content-Type: $type |
441 |
574 $headers |
442 __EOF |
575 __EOF |
443 } |
576 } |
444 |
577 |
445 sub date1123(;$) { |
578 sub date1123(;$) { |
446 my @now = gmtime(@_ ? shift : time); |
579 my @now = gmtime(@_ ? shift : time); |
499 |
633 |
500 =item B<-a>|B<--address> I<address> |
634 =item B<-a>|B<--address> I<address> |
501 |
635 |
502 The address to listen on. (default: 0.0.0.0) |
636 The address to listen on. (default: 0.0.0.0) |
503 |
637 |
|
638 =item B<--[no]auth> |
|
639 |
|
640 Do (or do not) authorize all access. (default: do) |
|
641 |
504 =item B<--[no]daemon> |
642 =item B<--[no]daemon> |
505 |
643 |
506 Do (or do not) daemonize. (default: do) |
644 Do (or do not) daemonize. (default: do) |
507 |
645 |
508 =item B<--[no]debug> |
646 =item B<--[no]debug> |
509 |
647 |
510 Do (or do not) print debug information to STDOUT/ERR and logfile. (default: dont) |
648 Do (or do not) print debug information to STDOUT/ERR and logfile. (default: dont) |
511 |
649 |
512 =item B<--htpasswd> I<path> |
650 =item B<--htpasswd> I<path> |
513 |
651 |
514 Path to alternate htpasswd file (default: htpasswd) |
652 Path to alternate htpasswd file (default: htpasswd). |
515 |
653 |
516 =item B<-k>|B<--kill> |
654 =item B<-k>|B<--kill> |
517 |
655 |
518 With this option the corresponding (address/port) process gets killed. |
656 With this option the corresponding (address/port) process gets killed. |
519 (default: off) |
657 (default: off) |
523 The number of lines to show. (default: 10) |
661 The number of lines to show. (default: 10) |
524 |
662 |
525 =item B<-p>|B<--port> I<port> |
663 =item B<-p>|B<--port> I<port> |
526 |
664 |
527 The port to listen on. (default: 8080) |
665 The port to listen on. (default: 8080) |
|
666 |
|
667 =item B<--realm> I<realm> |
|
668 |
|
669 Alternate Name for the HTTP Authentication realm parameter (default: basename($0)) |
528 |
670 |
529 =item B<--[no]ssl> |
671 =item B<--[no]ssl> |
530 |
672 |
531 Enable (or disable) https connections (default: enabled) |
673 Enable (or disable) https connections (default: enabled) |
532 |
674 |