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: $!"; +} |