aboutsummaryrefslogtreecommitdiffstats
path: root/webserver
diff options
context:
space:
mode:
Diffstat (limited to 'webserver')
-rwxr-xr-xwebserver100
1 files changed, 78 insertions, 22 deletions
diff --git a/webserver b/webserver
index e5e040d..0fe2979 100755
--- a/webserver
+++ b/webserver
@@ -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: $!";
+}