diff options
Diffstat (limited to 'webserver')
| -rwxr-xr-x | webserver | 100 | 
1 files changed, 78 insertions, 22 deletions
| @@ -1,8 +1,8 @@  #!/usr/bin/perl -T  #---------------------------------------------------------------------- -# Let's Encrypt ACME client (minimal webserver for answering challenges) -# Copyright © 2015 Guilhem Moulin <guilhem@fripost.org> +# Let's Encrypt ACME client (webserver component) +# Copyright © 2015,2016 Guilhem Moulin <guilhem@fripost.org>  #  # This program is free software: you can redistribute it and/or modify  # it under the terms of the GNU General Public License as published by @@ -21,30 +21,86 @@  use strict;  use warnings; +# Usage: webserver FD +# +# fdopen(3) the file descriptor FD (corresponding to a listening +# socket), then keep accept(2)'ing connections and consider each +# connection as the verification of an ACME challenge, i.e., serve the +# requested challenge token. +# +# NOTE: one needs to chdir(2) to an appropriate ACME challenge directory +# before running this program, since challenge tokens are (on purpose) +# only looked for in the current directory. +# +# NOTE: one should run this program as an unprivileged user:group such +# as "www-data:www-data"; bind(2)'ing to a privileged port such as 80 is +# not a problem since FD can be bound as root prior to the execve(2). + +use Errno 'EINTR'; +use Socket qw/AF_INET AF_INET6/; + +# Untaint and fdopen(3) the listening socket +# TODO: we could even take multiple file descriptors and select(2) +# between them; this could be useful to listen on two sockets, one for +# INET and one for INET6 +(shift @ARGV // die) =~ /\A(\d+)\z/ or die; +open my $S, '+<&=', $1 or die "fdopen $1: $!";  my $ROOT = '/.well-known/acme-challenge'; -$_ = <STDIN> // exit; -my $proto  = s/ HTTP\/(1\.[01])\r\n\z// ? $1 : die "Error: Bad request\n"; -my $method = s/\A(GET|HEAD) //          ? $1 : die "Error: Bad request\n"; +close STDIN  or die "close: $!"; +close STDOUT or die "close: $!"; + +sub info($$$) { +    my ($sockaddr, $msg, $req) = @_; +    $req =~ s/\r\n\z// if defined $req; + +    # get a string representation of the peer's address +    my $fam = Socket::sockaddr_family($sockaddr); +    my (undef, $ip) = +        $fam == AF_INET  ? Socket::unpack_sockaddr_in($sockaddr)  : +        $fam == AF_INET6 ? Socket::unpack_sockaddr_in6($sockaddr) : +        die; +    my $addr = Socket::inet_ntop($fam, $ip); -# Consume the headers (and ignore them) -while (defined (my $h = <STDIN>)) { last if $h eq "\r\n" }; +    print STDERR $msg." from [$addr]".(defined $req ? ": $req" : "")."\n"; +} + +while (1) { +    my $sockaddr = accept(my $conn, $S) or do { +        next if $! == EINTR; # try again if accept(2) was interrupted by a signal +        die "accept: $!"; +    }; +    my $req = $conn->getline(); +    info($sockaddr, "[$$] Incoming connection", $req) if $ENV{DEBUG}; +    next unless defined $req; -my ($status_line, $content_type, $content); -if (/\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/ and -f $1) { -    if (open my $fh, '<', $1) { -        ($status_line, $content_type) = ('200 OK', 'application/jose+json'); -        $content = do { local $/ = undef; <$fh> }; -        close $fh; +    if ($req !~ s/\A(GET|HEAD) (.*) HTTP\/(1\.[01])\r\n\z/$2/) { +        info($sockaddr, "Error: Bad request", $req); +        next;      } -    else { -        $status_line = '403 Forbidden'; +    my ($method, $proto) = ($1, $3); + +    # Consume (and ignore) the headers +    while (defined (my $h = $conn->getline())) { last if $h eq "\r\n" }; + +    my ($status_line, $content_type, $content); +    if ($req =~ /\A\Q$ROOT\E\/([A-Za-z0-9_\-]+)\z/ and -f $1) { +        if (open my $fh, '<', $1) { # only open files in the cwd +            ($status_line, $content_type) = ('200 OK', 'application/jose+json'); +            $content = do { local $/ = undef; $fh->getline() }; +            $fh->close() or die "close: $!"; +        } +        else { +            $status_line = '403 Forbidden'; +        }      } -} -print "HTTP/$proto ", ($status_line // '404 Not Found'), "\r\n"; -print "Content-Type: $content_type\r\n" if defined $content_type; -print "Content-Length: ".length($content)."\r\n" if defined $content; -print "Connection: close\r\n"; -print "\r\n"; -print $content if defined $content and $method eq 'GET'; +    $conn->print( "HTTP/$proto ", ($status_line // '404 Not Found'), "\r\n" ); +    $conn->print( "Content-Type: $content_type\r\n" ) if defined $content_type; +    $conn->print( "Content-Length: ".length($content)."\r\n" ) if defined $content; +    $conn->print( "Connection: close\r\n" ); +    $conn->print( "\r\n" ); +    $conn->print( $content ) if defined $content and $method eq 'GET'; + +    $conn->close() or die "close: $!"; +} | 
