hlog.pl
branchfoerste
changeset 40 99e8455f50dc
parent 39 22104f5d42ca
child 42 97c0f39be179
equal deleted inserted replaced
39:22104f5d42ca 40:99e8455f50dc
    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);
   477          [--[no]debug] 
   610          [--[no]debug] 
   478 	 [-k|--kill]
   611 	 [-k|--kill]
   479          [-a|--address address] [-p|--port port]
   612          [-a|--address address] [-p|--port port]
   480 	 [--lines n] 
   613 	 [--lines n] 
   481          [--htpasswd path]
   614          [--htpasswd path]
       
   615          [--realm realm]
   482          [--[no]ssl]
   616          [--[no]ssl]
   483          [--ssl-cert path]
   617          [--ssl-cert path]
   484          [--ssl-key path]
   618          [--ssl-key path]
   485 	 {file|tag=file ...}
   619 	 {file|tag=file ...}
   486 
   620 
   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 
   575 beeing used. For safety the hostname will be sanitized to avoid
   717 beeing used. For safety the hostname will be sanitized to avoid
   576 dangerous filenames.
   718 dangerous filenames.
   577 
   719 
   578 =head1 BUGS / TODO
   720 =head1 BUGS / TODO
   579 
   721 
   580 This tool should understand basic HTTP authentication.
   722 No known bugs.
   581 
   723 
   582 =cut
   724 =cut