hlog.pl
changeset 49 29532c7f9629
parent 48 2d6cb4466fb6
child 53 807117b2de7e
child 58 3f0838843487
equal deleted inserted replaced
29:754393593d11 49:29532c7f9629
    19 #    Heiko Schlittermann <hs@schlittermann.de>
    19 #    Heiko Schlittermann <hs@schlittermann.de>
    20 
    20 
    21 use strict;
    21 use strict;
    22 use warnings;
    22 use warnings;
    23 use Getopt::Long;
    23 use Getopt::Long;
    24 use IO::Socket::INET;
       
    25 use Pod::Usage;
    24 use Pod::Usage;
    26 use File::Basename;
    25 use File::Basename;
       
    26 use if $ENV{DEBUG} => "Smart::Comments";
    27 use POSIX qw(:sys_wait_h setsid);
    27 use POSIX qw(:sys_wait_h setsid);
    28 use Cwd;
    28 use Cwd qw(abs_path getcwd);
    29 
    29 use Authen::Simple::Passwd;
    30 my $opt_addr   = "0.0.0.0";
    30 use MIME::Base64 qw(decode_base64);
    31 my $opt_port   = 8080;
    31 use IO::Socket::INET;
    32 my $opt_lines  = 10;
    32 use IO::Socket::SSL;
    33 my $opt_daemon = 1;
       
    34 my $opt_kill   = 0;
       
    35 
    33 
    36 my $ME = basename $0;
    34 my $ME = basename $0;
    37 
    35 
       
    36 my $opt_addr     = "0.0.0.0";
       
    37 my $opt_auth     = $ME;
       
    38 my $opt_port     = 8080;
       
    39 my $opt_lines    = 10;
       
    40 my $opt_daemon   = 1;
       
    41 my $opt_kill     = 0;
       
    42 my $opt_debug    = 0;
       
    43 my $opt_htpasswd = "htpasswd";
       
    44 my $opt_ssl      = 1;
       
    45 my $opt_ssl_cert = "crt.pem";
       
    46 my $opt_ssl_key  = "key.pem";
       
    47 
    38 # these vars will be filled with the real dirs later
    48 # these vars will be filled with the real dirs later
    39 my $rundir = [ "/var/run/$ME", "$ENV{HOME}/.$ME" ];
    49 my $rundir = ["/var/run/$ME", "$ENV{HOME}/.$ME"];
    40 my $logdir = [ "/var/log/$ME", "$ENV{HOME}/.$ME" ];
    50 my $logdir = ["/var/log/$ME", "$ENV{HOME}/.$ME"];
    41 
    51 
    42 my $maxlogsize  = 1000_000_000;    # ca 1 MByte
    52 my $maxlogsize  = 1_000_000;    # ca 1 MByte
    43 my $killtimeout = 3;
    53 my $killtimeout = 3;
    44 
    54 
    45 # these are refs to detect if they're converted already
    55 # these are refs to detect if they're converted already
    46 my $access  = \"%s/access.log";
    56 my $access  = \"%s/access.log";
    47 my $errors  = \"%s/error.log";
    57 my $errors  = \"%s/error.log";
    48 my $pidfile = \"%s/%s.%s.pid";     # %dir/%ip.%port
    58 my $pidfile = \"%s/%s.%s.pid";    # %dir/%ip.%port
       
    59 
       
    60 # remember the pid that is actually written to the pid file so we can ensure
       
    61 # that only the process with that pid is attempting to remove the pidfile at
       
    62 # exit
       
    63 my $masterpid;
    49 
    64 
    50 END {
    65 END {
    51     unlink $pidfile
    66     unlink $pidfile
    52       if defined $pidfile and not ref $pidfile;
    67       if defined $pidfile
       
    68           and not ref $pidfile
       
    69           and defined $masterpid
       
    70           and $masterpid == $$;
    53 }
    71 }
    54 
    72 
    55 sub find_writable_dir(@);
    73 sub find_writable_dir(@);
    56 
    74 
    57 sub log_open($);
    75 sub log_open($);
    61 sub http($@);
    79 sub http($@);
    62 
    80 
    63 sub bad_request();
    81 sub bad_request();
    64 sub date1123(;$);
    82 sub date1123(;$);
    65 
    83 
       
    84 sub authenticated($$);
       
    85 
    66 my %FILE;
    86 my %FILE;
    67 
    87 
    68 MAIN: {
    88 MAIN: {
    69 
    89 
    70     GetOptions(
    90     GetOptions(
    71         "addr=s"  => \$opt_addr,
    91         "addr=s" => \$opt_addr,
       
    92         "auth:s" => sub { $opt_auth = $_[1] eq '' ? $ME : $_[1] },
       
    93         "noauth"  => sub { undef $opt_auth },
    72         "port=i"  => \$opt_port,
    94         "port=i"  => \$opt_port,
    73         "lines=i" => \$opt_lines,
    95         "lines=i" => \$opt_lines,
    74         "daemon!" => \$opt_daemon,
    96         "daemon!" => \$opt_daemon,
       
    97         "debug!"  => \$opt_debug,
    75         "kill"    => \$opt_kill,
    98         "kill"    => \$opt_kill,
    76         "help"    => sub { pod2usage(-verbose => 1, -exitval => 0) },
    99         "help" => sub { pod2usage(-verbose => 1, -exitval => 0) },
    77         "man"     => sub { pod2usage(-verbose => 2, -exitval => 0) },
   100         "man"  => sub { pod2usage(-verbose => 2, -exitval => 0) },
       
   101         "htpasswd=s" => \$opt_htpasswd,
       
   102         "ssl!"       => \$opt_ssl,
       
   103         "ssl-cert=s" => \$opt_ssl_cert,
       
   104         "ssl-key=s"  => \$opt_ssl_key
    78     ) or pod2usage();
   105     ) or pod2usage();
       
   106 
       
   107     if ($opt_kill) {
       
   108         $opt_auth = 0;
       
   109         $opt_ssl  = 0;
       
   110     }
       
   111 
       
   112     foreach ($opt_htpasswd, $opt_ssl_key, $opt_ssl_cert) {
       
   113         $_ = abs_path($_) if defined;
       
   114     }
       
   115 
       
   116     ### $opt_ssl_key
       
   117     ### $opt_ssl_cert
       
   118     ### $opt_auth
    79 
   119 
    80     if (defined($logdir = find_writable_dir(@$logdir))) {
   120     if (defined($logdir = find_writable_dir(@$logdir))) {
    81         $access = sprintf $$access, $logdir;
   121         $access = sprintf $$access, $logdir;
    82         $errors = sprintf $$errors, $logdir;
   122         $errors = sprintf $$errors, $logdir;
    83         log_open($access);
   123         log_open($access);
   117     pod2usage() if not @ARGV;
   157     pod2usage() if not @ARGV;
   118 
   158 
   119     # resolve tags and filenames
   159     # resolve tags and filenames
   120     foreach (@ARGV) {
   160     foreach (@ARGV) {
   121         $_ = "default=$_" if not /=/ or /^\//;
   161         $_ = "default=$_" if not /=/ or /^\//;
       
   162 
   122         my ($tag, $file) = split /=/, $_, 2;
   163         my ($tag, $file) = split /=/, $_, 2;
       
   164 
   123         die "tag $tag already exists with file $FILE{$tag}\n"
   165         die "tag $tag already exists with file $FILE{$tag}\n"
   124           if exists $FILE{$tag};
   166           if exists $FILE{$tag};
   125         $file = getcwd() . "/$file" if $file !~ /^\//;
   167         $file = abs_path($file);
   126         $FILE{$tag} = $file;
   168         $FILE{$tag} = $file;
   127     }
   169     }
   128 
   170 
   129     # start the listener
   171     # Start the listener, just a normal INET socket,
       
   172     # SSL will be started later on, if needed..
   130     my $listener = new IO::Socket::INET(
   173     my $listener = new IO::Socket::INET(
   131         LocalAddr => $opt_addr,
   174         LocalAddr => $opt_addr,
   132         LocalPort => $opt_port,
   175         LocalPort => $opt_port,
   133         Proto     => "tcp",
   176         Proto     => "tcp",
   134         Listen    => 1,
   177         Listen    => 1,
   135         ReuseAddr => 1,
   178         ReuseAddr => 1,
   136     ) or die "Can't create listener socket: $!\n";
   179     ) or die "Can't create listener: $!\n";
   137 
   180 
   138     # go daemon
   181     # go daemon
   139     chdir("/") or die "Can't chdir to /: $!\n";
   182     chdir("/") or die "Can't chdir to /: $!\n";
   140 
   183 
   141     if ($opt_daemon) {
   184     if ($opt_daemon) {
   145         # parent
   188         # parent
   146         if ($pid) {
   189         if ($pid) {
   147             print "listener $pid "
   190             print "listener $pid "
   148               . $listener->sockhost . ":"
   191               . $listener->sockhost . ":"
   149               . $listener->sockport . "\n";
   192               . $listener->sockport . "\n";
   150             undef $pidfile;
       
   151             exit 0;
   193             exit 0;
   152         }
   194         }
   153 
   195 
   154         # child
   196         # child
   155         setsid() or die "Can't start a new session: $!\n";
   197         setsid() or die "Can't start a new session: $!\n";
   172     if (defined $pidfile) {
   214     if (defined $pidfile) {
   173         open(PID, ">$pidfile")
   215         open(PID, ">$pidfile")
   174           or die "Can't open $pidfile: $!\n";
   216           or die "Can't open $pidfile: $!\n";
   175 
   217 
   176         print PID "$$\n";
   218         print PID "$$\n";
       
   219         $masterpid = $$;
   177         close PID;
   220         close PID;
   178     }
   221     }
   179 
   222 
   180     $SIG{CHLD} = sub {
   223     $SIG{CHLD} = sub {
   181         while (waitpid(-1, WNOHANG) > 0) {
   224         while (waitpid(-1, WNOHANG) > 0) {
   188 
   231 
   189         my $pid = fork();
   232         my $pid = fork();
   190         die "Can't fork: $!\n" if not defined $pid;
   233         die "Can't fork: $!\n" if not defined $pid;
   191         if ($pid == 0) {
   234         if ($pid == 0) {
   192             $SIG{CHLD} = "DEFAULT";
   235             $SIG{CHLD} = "DEFAULT";
   193             $listener->close;
   236             $listener->close();
       
   237             if ($opt_ssl) {
       
   238                 $client = IO::Socket::SSL->new_from_fd(
       
   239                     $client,
       
   240                     SSL_server    => 1,
       
   241                     SSL_key_file  => $opt_ssl_key,
       
   242                     SSL_cert_file => $opt_ssl_cert,
       
   243                 );
       
   244                 $client->start_SSL;
       
   245             }
   194             handle_request($client);
   246             handle_request($client);
   195             exit 0;
   247             exit 0;
   196         }
   248         }
   197         $client->close;
   249         $client->close();
   198 
   250 
   199         # maintenance of logfiles
   251         # maintenance of logfiles
   200         if (-s $access > $maxlogsize) {
   252         if (-s $access > $maxlogsize) {
   201             rename $access, "$access.1";
   253             rename $access, "$access.1";
   202             log_open($access);
   254             log_open($access);
   232 
   284 
   233 }
   285 }
   234 
   286 
   235 sub handle_request($) {
   287 sub handle_request($) {
   236     my $client = shift;
   288     my $client = shift;
       
   289 
   237     local $_ = <$client>;
   290     local $_ = <$client>;
   238 
   291 
   239     # should be HTTP/x.x
   292     # should be HTTP(S)/x.x
   240     if (not s/\s+HTTP\/\S+\s*$//) {
   293     if (not s/\s+HTTPS?\/\S+\s*$//) {
       
   294         log_write("Bad Request: $_") if $opt_debug;
   241         $client->print(bad_request);
   295         $client->print(bad_request);
   242         return;
   296         return;
   243     }
   297     }
   244 
   298 
   245     # should be a GET request
   299     # should be a GET request
   246     if (not s/GET\s+//) {
   300     if (not s/GET\s+//) {
       
   301         log_write("Bad Request: $_") if $opt_debug;
   247         $client->print(http "400 Bad Request" => bad_request);
   302         $client->print(http "400 Bad Request" => bad_request);
   248     }
   303     }
   249 
   304 
   250     # number of lines and tag to show
   305     # number of lines and tag to show
   251     my $lines = (s/(\d+)$//    ? $1 : $opt_lines);
   306     my $lines = (s/(\d+)$//    ? $1 : $opt_lines);
   252     my $tag   = (s/^\/*(\w+)// ? $1 : "default");
   307     my $tag   = (s/^\/*(\w+)// ? $1 : "default");
   253 
   308 
   254     # read the header(s) and discard
   309     my $authenticated = defined $opt_auth ? 0 : 1;
   255     while (<$client>) { last if /^\s*$/ }
   310     ### $authenticated
       
   311 
       
   312    # read and verify (first) authentication header and discard any other headers
       
   313     while (<$client>) {
       
   314         last if /^\s*$/;
       
   315         next if $authenticated;
       
   316 
       
   317         if (/^Authorization:\s+Basic\s+([[:alnum:]+\/=]+)\r?$/) {
       
   318             $authenticated = authenticate($opt_htpasswd => $1)
       
   319               or log_write("authentication failure from " . $client->peerhost);
       
   320         }
       
   321 
       
   322     }
       
   323     ### $authenticated
       
   324 
       
   325     unless ($authenticated) {
       
   326 
       
   327         $client->print(
       
   328             http {
       
   329                 code => "401 Unauthorized",
       
   330                 headers =>
       
   331                   { "WWW-Authenticate" => "Basic realm=\"$opt_auth\"", }
       
   332             },
       
   333             "not authorized"
       
   334         );
       
   335         return;
       
   336 
       
   337     }
   256 
   338 
   257     if (not exists $FILE{$tag}) {
   339     if (not exists $FILE{$tag}) {
   258         $client->print(http "500 unknown file tag",
   340         $client->print(http "500 unknown file tag",
   259             "Sorry, unknown file tag \"$tag\"");
   341             "Sorry, unknown file tag \"$tag\"");
   260         log_write("unknown tag $tag");
   342         log_write("unknown tag $tag");
   268     }
   350     }
   269 
   351 
   270     log_write($client->peerhost . ":" . $client->peerport . " $tag ($lines)");
   352     log_write($client->peerhost . ":" . $client->peerport . " $tag ($lines)");
   271 
   353 
   272     seek($file{fh}, -($lines + 1) * $file{avglen}, 2);
   354     seek($file{fh}, -($lines + 1) * $file{avglen}, 2);
   273     $file{fh}->getline;
   355 
       
   356     # warum das? $file{fh}->getline;
   274 
   357 
   275     $client->print(http "200 OK" => join "", <<__EOF, $file{fh}->getlines);
   358     $client->print(http "200 OK" => join "", <<__EOF, $file{fh}->getlines);
   276 # Proof of concept ;-)
   359 # Proof of concept ;-)
   277 # see https://keller.schlittermann.de/hg/hlog
   360 # see https://keller.schlittermann.de/hg/hlog
   278 #
   361 #
   314     seek($r{fh}, 0, 0);
   397     seek($r{fh}, 0, 0);
   315     return %r;
   398     return %r;
   316 }
   399 }
   317 
   400 
   318 sub http($@) {
   401 sub http($@) {
   319     my $code = shift;
   402 
   320     my $date = date1123();
   403     my ($headers, $code, $date) = ('');
       
   404 
       
   405     if (ref $_[0] eq "HASH") {
       
   406 
       
   407         my $h;
       
   408         ($code, $date, $h) = @{ $_[0] }{ 'code', 'date', 'headers' };
       
   409         $headers = (join "\n", map { "$_: $h->{$_}" } keys %{$h}) . "\n"
       
   410           if defined $h;
       
   411         shift;
       
   412 
       
   413     }
       
   414     else {
       
   415 
       
   416         $code = shift;
       
   417 
       
   418     }
       
   419 
       
   420     $date ||= date1123();
   321 
   421 
   322     my $type = $_[0] =~ /^<!DOCTYPE HTML/ ? "text/html" : "text/plain";
   422     my $type = $_[0] =~ /^<!DOCTYPE HTML/ ? "text/html" : "text/plain";
   323 
   423 
   324     return <<__EOF, @_;
   424     return <<__EOF, @_;
   325 HTTP/1.1 $code
   425 HTTP/1.1 $code
   326 Date: $date
   426 Date: $date
   327 Connection: close
   427 Connection: close
   328 Content-Type: $type
   428 Content-Type: $type
   329 
   429 $headers
   330 __EOF
   430 __EOF
   331 }
   431 }
   332 
   432 
   333 sub date1123(;$) {
   433 sub date1123(;$) {
   334     my @now = gmtime(@_ ? shift : time);
   434     my @now = gmtime(@_ ? shift : time);
   335     sprintf "%s, %2d %s %4d %02d:%02d:%02d GMT",
   435     sprintf "%s, %2d %s %4d %02d:%02d:%02d GMT",
   336       qw(Sun Mon Tue Wed Thu Fri Sat Sun) [ $now[6] ],
   436       qw(Sun Mon Tue Wed Thu Fri Sat Sun) [$now[6]],
   337       $now[3],
   437       $now[3],
   338       qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) [ $now[4] ],
   438       qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) [$now[4]],
   339       $now[5] + 1900, @now[ 2, 1, 0 ];
   439       $now[5] + 1900, @now[2, 1, 0];
   340 }
   440 }
   341 
   441 
   342 sub bad_request() {
   442 sub bad_request() {
   343     return <<'__EOF';
   443     return <<'__EOF';
   344 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
   444 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
   351 </p>
   451 </p>
   352 </body></html>
   452 </body></html>
   353 __EOF
   453 __EOF
   354 }
   454 }
   355 
   455 
       
   456 sub authenticate($$) {
       
   457     my ($htpasswd, $userinfo) = @_;
       
   458     my $auth = new Authen::Simple::Passwd(path => $htpasswd)
       
   459       or die "Can't open \"$htpasswd\": $!\n";
       
   460     $auth->authenticate(split /:/, decode_base64($userinfo));
       
   461 }
       
   462 
   356 __END__
   463 __END__
   357 
   464 
   358 =head1 NAME
   465 =head1 NAME
   359 
   466 
   360 hlog - simple http server providing access to some logfile
   467 hlog - simple http server providing access to some logfile
   361 
   468 
   362 =head1 SYNOPSIS
   469 =head1 SYNOPSIS
   363     
   470     
   364     hlog [--[no]daemon] 
   471     hlog [--[no]daemon]
       
   472          [--[no]debug] 
   365 	 [-k|--kill]
   473 	 [-k|--kill]
   366          [-a|--address address] [-p|--port port]
   474          [-a|--address address] [-p|--port port]
   367 	 [--lines n] 
   475 	 [--lines n] 
       
   476          [--htpasswd path]
       
   477          [--[no]ssl]
       
   478 	 [--auth=[realm] | --noauth]
       
   479          [--ssl-cert path]
       
   480          [--ssl-key path]
   368 	 {file|tag=file ...}
   481 	 {file|tag=file ...}
   369 
   482 
   370     hlog [-h|--help] [-m|--man]
   483     hlog [-h|--help] [-m|--man]
   371 
   484 
   372 =head1 DESCRIPTION
   485 =head1 DESCRIPTION
   373 
   486 
   374 This script should run as a server providing access to 
   487 This script should run as a server providing access to 
   375 the last lines of a logfile. It should understand basic HTTP/1.x.
   488 the last lines of a logfile. It understands basic HTTP(S)/1.x.
   376 
   489 
   377 See the L<FILES> section for more information on files.
   490 See the L<FILES> section for more information on files.
   378 
   491 
   379 =head1 OPTIONS
   492 =head1 OPTIONS
   380 
   493 
   382 
   495 
   383 =item B<-a>|B<--address> I<address>
   496 =item B<-a>|B<--address> I<address>
   384 
   497 
   385 The address to listen on. (default: 0.0.0.0)
   498 The address to listen on. (default: 0.0.0.0)
   386 
   499 
       
   500 =item B<--auth>[ I<realm>] | B<--noauth>
       
   501 
       
   502 Do (or do not) authorize all access. Optional you may pass the 
       
   503 name of a authentication realm. (default: do, realm is hlog)
       
   504 
   387 =item B<--[no]daemon>
   505 =item B<--[no]daemon>
   388 
   506 
   389 Do (or do not) daemonize. (default: do)
   507 Do (or do not) daemonize. (default: do)
   390 
   508 
   391 =item B<--lines> I<lines>
   509 =item B<--[no]debug>
   392 
   510 
   393 The number of lines to show. (default: 10)
   511 Do (or do not) print debug information to STDOUT/ERR and logfile. (default: dont)
       
   512 
       
   513 =item B<--htpasswd> I<path>
       
   514 
       
   515 Path to alternate htpasswd file (default: htpasswd).
   394 
   516 
   395 =item B<-k>|B<--kill>
   517 =item B<-k>|B<--kill>
   396 
   518 
   397 With this option the corresponding (address/port) process gets killed.
   519 With this option the corresponding (address/port) process gets killed.
   398 (default: off)
   520 (default: off)
   399 
   521 
       
   522 =item B<--lines> I<lines>
       
   523 
       
   524 The number of lines to show. (default: 10)
       
   525 
   400 =item B<-p>|B<--port> I<port>
   526 =item B<-p>|B<--port> I<port>
   401 
   527 
   402 The port to listen on. (default: 8080)
   528 The port to listen on. (default: 8080)
       
   529 
       
   530 =item B<--[no]ssl>
       
   531 
       
   532 Enable (or disable) https connections (default: enabled)
       
   533 
       
   534 =item B<--ssl-cert> I<path>
       
   535 
       
   536 Path to alternate ssl certificate file (default: crt.pem)
       
   537 
       
   538 =item B<--ssl-key> I<path>
       
   539 
       
   540 Path to alternate ssl private key file (default: key.pem)
   403 
   541 
   404 =back
   542 =back
   405 
   543 
   406 =head1 EXAMPLES
   544 =head1 EXAMPLES
   407 
   545 
   436 
   574 
   437 The pid file will be named according to the hostname (see B<--address>)
   575 The pid file will be named according to the hostname (see B<--address>)
   438 beeing used. For safety the hostname will be sanitized to avoid
   576 beeing used. For safety the hostname will be sanitized to avoid
   439 dangerous filenames.
   577 dangerous filenames.
   440 
   578 
       
   579 =head1 BUGS / TODO
       
   580 
       
   581 No known bugs.
       
   582 
   441 =cut
   583 =cut