hlog.pl
branchfoerste
changeset 34 5265573638b2
parent 32 187167ae27e5
child 35 f237843486d2
equal deleted inserted replaced
33:8f0cb57a65d9 34:5265573638b2
    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;
    27 use POSIX qw(:sys_wait_h setsid);
    26 use POSIX qw(:sys_wait_h setsid);
    28 use Cwd;
    27 use Cwd;
    29 
    28 
    30 my $opt_addr   = "0.0.0.0";
    29 my $opt_addr     = "0.0.0.0";
    31 my $opt_port   = 8080;
    30 my $opt_port     = 8080;
    32 my $opt_lines  = 10;
    31 my $opt_lines    = 10;
    33 my $opt_daemon = 1;
    32 my $opt_daemon   = 1;
    34 my $opt_kill   = 0;
    33 my $opt_kill     = 0;
       
    34 my $opt_debug    = 0;
       
    35 my $opt_ssl      = 1;
       
    36 my $opt_ssl_cert = "crt.pem";
       
    37 my $opt_ssl_key  = "key.pem";
    35 
    38 
    36 my $ME = basename $0;
    39 my $ME = basename $0;
    37 
    40 
    38 # these vars will be filled with the real dirs later
    41 # these vars will be filled with the real dirs later
    39 my $rundir = [ "/var/run/$ME", "$ENV{HOME}/.$ME" ];
    42 my $rundir = [ "/var/run/$ME", "$ENV{HOME}/.$ME" ];
    40 my $logdir = [ "/var/log/$ME", "$ENV{HOME}/.$ME" ];
    43 my $logdir = [ "/var/log/$ME", "$ENV{HOME}/.$ME" ];
    41 
    44 
    42 my $maxlogsize  = 1000_000_000;    # ca 1 MByte
    45 my $maxlogsize  = 1_000_000;    # ca 1 MByte
    43 my $killtimeout = 3;
    46 my $killtimeout = 3;
    44 
    47 
    45 # these are refs to detect if they're converted already
    48 # these are refs to detect if they're converted already
    46 my $access  = \"%s/access.log";
    49 my $access  = \"%s/access.log";
    47 my $errors  = \"%s/error.log";
    50 my $errors  = \"%s/error.log";
    48 my $pidfile = \"%s/%s.%s.pid";     # %dir/%ip.%port
    51 my $pidfile = \"%s/%s.%s.pid";     # %dir/%ip.%port
    49 
    52 
       
    53 # remember the pid that is actually written to the pid file so we can ensure
       
    54 # that only the process with that pid is attempting to remove the pidfile at
       
    55 # exit
       
    56 my $masterpid;
       
    57 
       
    58 
       
    59 # attempt to generalize some interface differences between
       
    60 # IO::Socket::{INET,SSL}; currently we need to pass an SSL specific argument
       
    61 # when closing an SSL Socket to avoid affecting the socket in
       
    62 # parent(s)/children; passing unknown arguments to the 'close' method of non
       
    63 # SSL Sockets would result in an runtime error; error reporting is also done
       
    64 # differently; note that you cant mix non/SSL Sockets since currently we
       
    65 # achieve it by modifying @ISA which is a class variable in the constructor;
       
    66 # the right thing to do would probably be to use IO::Socket::SSL for non SSL
       
    67 # Sockets too -> TODO: how? ;)
       
    68 package IO::Socket::hlog;
       
    69 
       
    70 use IO::Socket::INET;
       
    71 use IO::Socket::SSL;
       
    72 
       
    73 our (@ISA, $DEBUG);
       
    74 
       
    75 sub new {
       
    76 
       
    77   my $class = shift;
       
    78   
       
    79   my %args = @_;
       
    80   my $ssl = delete $args{SSL};
       
    81 
       
    82   if ($ssl) {
       
    83 
       
    84     @ISA = qw(IO::Socket::SSL);
       
    85     $IO::Socket::SSL::DEBUG = $DEBUG ? 3 : 0;
       
    86 
       
    87   } else {
       
    88 
       
    89     %args = _delete_ssl_args(%args);
       
    90     @ISA = qw(IO::Socket::INET);
       
    91 
       
    92   }
       
    93 
       
    94   my $self;
       
    95   unless ($self = $class->SUPER::new(%args)) {
       
    96 
       
    97     return;
       
    98 
       
    99   }
       
   100 
       
   101   print "$class: $self created\n" if $DEBUG;
       
   102 
       
   103   return $self;
       
   104 
       
   105 }
       
   106 
       
   107 sub close {
       
   108 
       
   109   my $self = shift;
       
   110   print "$self: closing\n" if $DEBUG;
       
   111 
       
   112   my %args = @_;
       
   113 
       
   114   %args = _delete_ssl_args(%args) unless $self->_is_ssl;
       
   115 
       
   116   return $self->SUPER::close(%args);
       
   117 
       
   118 }
       
   119 
       
   120 sub errstr {
       
   121 
       
   122   return IO::Socket::SSL::errstr if _is_ssl();
       
   123 
       
   124   return $@;
       
   125 
       
   126 }
       
   127 
       
   128 sub _delete_ssl_args {
       
   129 
       
   130   my %args = @_;
       
   131   map { delete $args{$_} if /^SSL/; } keys %args;
       
   132   return %args;
       
   133 
       
   134 }
       
   135 
       
   136 sub _is_ssl {
       
   137 
       
   138   my $self = shift;
       
   139   return $ISA[0] eq "IO::Socket::SSL";
       
   140 
       
   141 }
       
   142 
       
   143 package main;
       
   144 
    50 END {
   145 END {
    51     unlink $pidfile
   146     unlink $pidfile
    52       if defined $pidfile and not ref $pidfile;
   147       if defined $pidfile and not ref $pidfile and defined $masterpid and $masterpid == $$;
    53 }
   148 }
    54 
   149 
    55 sub find_writable_dir(@);
   150 sub find_writable_dir(@);
    56 
   151 
    57 sub log_open($);
   152 sub log_open($);
    66 my %FILE;
   161 my %FILE;
    67 
   162 
    68 MAIN: {
   163 MAIN: {
    69 
   164 
    70     GetOptions(
   165     GetOptions(
    71         "addr=s"  => \$opt_addr,
   166         "addr=s"     => \$opt_addr,
    72         "port=i"  => \$opt_port,
   167         "port=i"     => \$opt_port,
    73         "lines=i" => \$opt_lines,
   168         "lines=i"    => \$opt_lines,
    74         "daemon!" => \$opt_daemon,
   169         "daemon!"    => \$opt_daemon,
    75         "kill"    => \$opt_kill,
   170         "debug!"     => \$opt_debug,
    76         "help"    => sub { pod2usage(-verbose => 1, -exitval => 0) },
   171         "kill"       => \$opt_kill,
    77         "man"     => sub { pod2usage(-verbose => 2, -exitval => 0) },
   172         "help"       => sub { pod2usage(-verbose => 1, -exitval => 0) },
       
   173         "man"        => sub { pod2usage(-verbose => 2, -exitval => 0) },
       
   174         "ssl!"       => \$opt_ssl,
       
   175         "ssl-cert=s" => \$opt_ssl_cert,
       
   176         "ssl-key=s"  => \$opt_ssl_key
    78     ) or pod2usage();
   177     ) or pod2usage();
       
   178 
       
   179     $IO::Socket::hlog::DEBUG = $opt_debug;
    79 
   180 
    80     if (defined($logdir = find_writable_dir(@$logdir))) {
   181     if (defined($logdir = find_writable_dir(@$logdir))) {
    81         $access = sprintf $$access, $logdir;
   182         $access = sprintf $$access, $logdir;
    82         $errors = sprintf $$errors, $logdir;
   183         $errors = sprintf $$errors, $logdir;
    83         log_open($access);
   184         log_open($access);
   125         $file = getcwd() . "/$file" if $file !~ /^\//;
   226         $file = getcwd() . "/$file" if $file !~ /^\//;
   126         $FILE{$tag} = $file;
   227         $FILE{$tag} = $file;
   127     }
   228     }
   128 
   229 
   129     # start the listener
   230     # start the listener
   130     my $listener = new IO::Socket::INET(
   231     my $listener = new IO::Socket::hlog(
   131         LocalAddr => $opt_addr,
   232         LocalAddr     => $opt_addr,
   132         LocalPort => $opt_port,
   233         LocalPort     => $opt_port,
   133         Proto     => "tcp",
   234         Proto         => "tcp",
   134         Listen    => 1,
   235         Listen        => 1,
   135         ReuseAddr => 1,
   236         ReuseAddr     => 1,
   136     ) or die "Can't create listener socket: $!\n";
   237         SSL           => $opt_ssl,
       
   238         SSL_key_file  => $opt_ssl_key, 
       
   239         SSL_cert_file => $opt_ssl_cert,
       
   240         debug         => $opt_debug
       
   241     ) or die "Can't create listener socket: ", IO::Socket::hlog::errstr, "\n";
       
   242 
   137 
   243 
   138     # go daemon
   244     # go daemon
   139     chdir("/") or die "Can't chdir to /: $!\n";
   245     chdir("/") or die "Can't chdir to /: $!\n";
   140 
   246 
   141     if ($opt_daemon) {
   247     if ($opt_daemon) {
   145         # parent
   251         # parent
   146         if ($pid) {
   252         if ($pid) {
   147             print "listener $pid "
   253             print "listener $pid "
   148               . $listener->sockhost . ":"
   254               . $listener->sockhost . ":"
   149               . $listener->sockport . "\n";
   255               . $listener->sockport . "\n";
   150             undef $pidfile;
       
   151             exit 0;
   256             exit 0;
   152         }
   257         }
   153 
   258 
   154         # child
   259         # child
   155         setsid() or die "Can't start a new session: $!\n";
   260         setsid() or die "Can't start a new session: $!\n";
   172     if (defined $pidfile) {
   277     if (defined $pidfile) {
   173         open(PID, ">$pidfile")
   278         open(PID, ">$pidfile")
   174           or die "Can't open $pidfile: $!\n";
   279           or die "Can't open $pidfile: $!\n";
   175 
   280 
   176         print PID "$$\n";
   281         print PID "$$\n";
       
   282         $masterpid = $$;
   177         close PID;
   283         close PID;
   178     }
   284     }
   179 
   285 
   180     $SIG{CHLD} = sub {
   286     $SIG{CHLD} = sub {
   181         while (waitpid(-1, WNOHANG) > 0) {
   287         while (waitpid(-1, WNOHANG) > 0) {
   188 
   294 
   189         my $pid = fork();
   295         my $pid = fork();
   190         die "Can't fork: $!\n" if not defined $pid;
   296         die "Can't fork: $!\n" if not defined $pid;
   191         if ($pid == 0) {
   297         if ($pid == 0) {
   192             $SIG{CHLD} = "DEFAULT";
   298             $SIG{CHLD} = "DEFAULT";
   193             $listener->close;
   299         print("listener $listener\n") if $opt_debug;
       
   300             $listener->close(SSL_no_shutdown => 1);
   194             handle_request($client);
   301             handle_request($client);
   195             exit 0;
   302             exit 0;
   196         }
   303         }
   197         $client->close;
   304         print("client $client\n") if $opt_debug;
       
   305         $client->close(SSL_no_shutdown => 1);
   198 
   306 
   199         # maintenance of logfiles
   307         # maintenance of logfiles
   200         if (-s $access > $maxlogsize) {
   308         if (-s $access > $maxlogsize) {
   201             rename $access, "$access.1";
   309             rename $access, "$access.1";
   202             log_open($access);
   310             log_open($access);
   234 
   342 
   235 sub handle_request($) {
   343 sub handle_request($) {
   236     my $client = shift;
   344     my $client = shift;
   237     local $_ = <$client>;
   345     local $_ = <$client>;
   238 
   346 
   239     # should be HTTP/x.x
   347     # should be HTTP(S)/x.x
   240     if (not s/\s+HTTP\/\S+\s*$//) {
   348     if (not s/\s+HTTPS?\/\S+\s*$//) {
       
   349         log_write("Bad Request: $_") if $opt_debug;
   241         $client->print(bad_request);
   350         $client->print(bad_request);
   242         return;
   351         return;
   243     }
   352     }
   244 
   353 
   245     # should be a GET request
   354     # should be a GET request
   246     if (not s/GET\s+//) {
   355     if (not s/GET\s+//) {
       
   356         log_write("Bad Request: $_") if $opt_debug;
   247         $client->print(http "400 Bad Request" => bad_request);
   357         $client->print(http "400 Bad Request" => bad_request);
   248     }
   358     }
   249 
   359 
   250     # number of lines and tag to show
   360     # number of lines and tag to show
   251     my $lines = (s/(\d+)$//    ? $1 : $opt_lines);
   361     my $lines = (s/(\d+)$//    ? $1 : $opt_lines);
   359 
   469 
   360 hlog - simple http server providing access to some logfile
   470 hlog - simple http server providing access to some logfile
   361 
   471 
   362 =head1 SYNOPSIS
   472 =head1 SYNOPSIS
   363     
   473     
   364     hlog [--[no]daemon] 
   474     hlog [--[no]daemon]
       
   475          [--[no]debug] 
   365 	 [-k|--kill]
   476 	 [-k|--kill]
   366          [-a|--address address] [-p|--port port]
   477          [-a|--address address] [-p|--port port]
   367 	 [--lines n] 
   478 	 [--lines n] 
       
   479          [--[no]ssl]
       
   480          [--ssl-cert path]
       
   481          [--ssl-key path]
   368 	 {file|tag=file ...}
   482 	 {file|tag=file ...}
   369 
   483 
   370     hlog [-h|--help] [-m|--man]
   484     hlog [-h|--help] [-m|--man]
   371 
   485 
   372 =head1 DESCRIPTION
   486 =head1 DESCRIPTION
   373 
   487 
   374 This script should run as a server providing access to 
   488 This script should run as a server providing access to 
   375 the last lines of a logfile. It should understand basic HTTP/1.x.
   489 the last lines of a logfile. It should understand basic HTTP(S)/1.x.
   376 
   490 
   377 See the L<FILES> section for more information on files.
   491 See the L<FILES> section for more information on files.
   378 
   492 
   379 =head1 OPTIONS
   493 =head1 OPTIONS
   380 
   494 
   385 The address to listen on. (default: 0.0.0.0)
   499 The address to listen on. (default: 0.0.0.0)
   386 
   500 
   387 =item B<--[no]daemon>
   501 =item B<--[no]daemon>
   388 
   502 
   389 Do (or do not) daemonize. (default: do)
   503 Do (or do not) daemonize. (default: do)
       
   504 
       
   505 =item B<--[no]debug>
       
   506 
       
   507 Do (or do not) print debug information to STDOUT/ERR and logfile. (default: dont)
   390 
   508 
   391 =item B<--lines> I<lines>
   509 =item B<--lines> I<lines>
   392 
   510 
   393 The number of lines to show. (default: 10)
   511 The number of lines to show. (default: 10)
   394 
   512 
   398 (default: off)
   516 (default: off)
   399 
   517 
   400 =item B<-p>|B<--port> I<port>
   518 =item B<-p>|B<--port> I<port>
   401 
   519 
   402 The port to listen on. (default: 8080)
   520 The port to listen on. (default: 8080)
       
   521 
       
   522 =item B<--[no]ssl>
       
   523 
       
   524 Enable (or disable) https connections (default: enabled)
       
   525 
       
   526 =back
       
   527 
       
   528 =item B<--ssl-cert>
       
   529 
       
   530 Path to alternate ssl certificate file (default: crt.pem)
       
   531 
       
   532 =back
       
   533 
       
   534 =item B<--ssl-key>
       
   535 
       
   536 Path to alternate ssl private key file (default: key.pem)
   403 
   537 
   404 =back
   538 =back
   405 
   539 
   406 =head1 EXAMPLES
   540 =head1 EXAMPLES
   407 
   541 
   438 beeing used. For safety the hostname will be sanitized to avoid
   572 beeing used. For safety the hostname will be sanitized to avoid
   439 dangerous filenames.
   573 dangerous filenames.
   440 
   574 
   441 =head1 BUGS / TODO
   575 =head1 BUGS / TODO
   442 
   576 
   443 This tool should understand HTTPS and basic HTTP authentication.
   577 This tool should understand basic HTTP authentication.
   444 
   578 
   445 =cut
   579 =cut