about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--lib/PublicInbox/Daemon.pm60
-rw-r--r--t/httpd-corner.t27
-rw-r--r--t/httpd-unix.t105
3 files changed, 172 insertions, 20 deletions
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index c101ecb7..9f33c05a 100644
--- a/lib/PublicInbox/Daemon.pm
+++ b/lib/PublicInbox/Daemon.pm
@@ -7,6 +7,7 @@ use strict;
 use warnings;
 use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
 use IO::Handle;
+use IO::Socket;
 STDOUT->autoflush(1);
 STDERR->autoflush(1);
 require Danga::Socket;
@@ -52,17 +53,35 @@ sub daemon_prepare ($) {
 
         foreach my $l (@cfg_listen) {
                 next if $listener_names{$l}; # already inherited
-                require IO::Socket::INET6; # works for IPv4, too
-                my %o = (
-                        LocalAddr => $l,
-                        ReuseAddr => 1,
-                        Proto => 'tcp',
-                );
-                if (my $s = IO::Socket::INET6->new(%o)) {
+                my (%o, $sock_pkg);
+                if (index($l, '/') == 0) {
+                        $sock_pkg = 'IO::Socket::UNIX';
+                        eval "use $sock_pkg";
+                        die $@ if $@;
+                        %o = (Type => SOCK_STREAM, Peer => $l);
+                        if (-S $l) {
+                                my $c = $sock_pkg->new(%o);
+                                if (!defined($c) && $!{ECONNREFUSED}) {
+                                        unlink $l or die
+"failed to unlink stale socket=$l: $!\n";
+                                } # else: let the bind fail
+                        }
+                        $o{Local} = delete $o{Peer};
+                } else {
+                        $sock_pkg = 'IO::Socket::INET6'; # works for IPv4, too
+                        eval "use $sock_pkg";
+                        die $@ if $@;
+                        %o = (LocalAddr => $l, ReuseAddr => 1, Proto => 'tcp');
+                }
+                $o{Listen} = 1024;
+                my $prev = umask 0000;
+                my $s = eval { $sock_pkg->new(%o) };
+                warn "error binding $l: $!\n" unless $s;
+                umask $prev;
+
+                if ($s) {
                         $listener_names{sockname($s)} = $s;
                         push @listeners, $s;
-                } else {
-                        warn "error binding $l: $!\n";
                 }
         }
         die "No listeners bound\n" unless @listeners;
@@ -165,15 +184,20 @@ sub sockname ($) {
 sub host_with_port ($) {
         my ($addr) = @_;
         my ($port, $host);
-        if (length($addr) >= 28) {
-                require Socket6;
-                ($port, $host) = Socket6::unpack_sockaddr_in6($addr);
-                $host = '['.Socket6::inet_ntop(Socket6::AF_INET6(), $host).']';
-        } else {
-                ($port, $host) = Socket::sockaddr_in($addr);
-                $host = Socket::inet_ntoa($host);
-        }
-        ($host, $port);
+
+        # this eval will die on Unix sockets:
+        eval {
+                if (length($addr) >= 28) {
+                        require Socket6;
+                        ($port, $host) = Socket6::unpack_sockaddr_in6($addr);
+                        $host = Socket6::inet_ntop(Socket6::AF_INET6(), $host);
+                        $host = "[$host]";
+                } else {
+                        ($port, $host) = Socket::sockaddr_in($addr);
+                        $host = Socket::inet_ntoa($host);
+                }
+        };
+        $@ ? ('127.0.0.1', 0) : ($host, $port);
 }
 
 sub inherit () {
diff --git a/t/httpd-corner.t b/t/httpd-corner.t
index 198a7e90..19564074 100644
--- a/t/httpd-corner.t
+++ b/t/httpd-corner.t
@@ -16,6 +16,7 @@ use Digest::SHA qw(sha1_hex);
 use File::Temp qw/tempdir/;
 use Cwd qw/getcwd/;
 use IO::Socket;
+use IO::Socket::UNIX;
 use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD :seek);
 use Socket qw(SO_KEEPALIVE IPPROTO_TCP TCP_NODELAY);
 use POSIX qw(dup2 mkfifo :sys_wait_h);
@@ -34,20 +35,32 @@ my %opts = (
         Listen => 1024,
 );
 my $sock = IO::Socket::INET->new(%opts);
+my $upath = "$tmpdir/s";
+my $unix = IO::Socket::UNIX->new(
+        Listen => 1024,
+        Type => SOCK_STREAM,
+        Local => $upath
+);
+ok($unix, 'UNIX socket created');
 my $pid;
 END { kill 'TERM', $pid if defined $pid };
 my $spawn_httpd = sub {
         my (@args) = @_;
+        $! = 0;
         my $fl = fcntl($sock, F_GETFD, 0);
         ok(! $!, 'no error from fcntl(F_GETFD)');
         is($fl, FD_CLOEXEC, 'cloexec set by default (Perl behavior)');
         $pid = fork;
         if ($pid == 0) {
                 # pretend to be systemd
-                fcntl($sock, F_SETFD, $fl &= ~FD_CLOEXEC);
                 dup2(fileno($sock), 3) or die "dup2 failed: $!\n";
+                dup2(fileno($unix), 4) or die "dup2 failed: $!\n";
+                $sock = IO::Handle->new_from_fd(3, 'r');
+                $sock->fcntl(F_SETFD, 0);
+                $unix = IO::Handle->new_from_fd(4, 'r');
+                $unix->fcntl(F_SETFD, 0);
                 $ENV{LISTEN_PID} = $$;
-                $ENV{LISTEN_FDS} = 1;
+                $ENV{LISTEN_FDS} = 2;
                 exec $httpd, @args, "--stdout=$out", "--stderr=$err", $psgi;
                 die "FAIL: $!\n";
         }
@@ -63,6 +76,16 @@ my $spawn_httpd = sub {
         $spawn_httpd->('-W0');
 }
 
+# Unix domain sockets
+{
+        my $u = IO::Socket::UNIX->new(Type => SOCK_STREAM, Peer => $upath);
+        ok($u, 'unix socket connected');
+        $u->write("GET /host-port HTTP/1.0\r\n\r\n");
+        $u->read(my $buf, 4096);
+        like($buf, qr!\r\n\r\n127\.0\.0\.1:0\z!,
+                'set REMOTE_ADDR and REMOTE_PORT for Unix socket');
+}
+
 sub conn_for {
         my ($sock, $msg) = @_;
         my $conn = IO::Socket::INET->new(
diff --git a/t/httpd-unix.t b/t/httpd-unix.t
new file mode 100644
index 00000000..580d14d2
--- /dev/null
+++ b/t/httpd-unix.t
@@ -0,0 +1,105 @@
+# Copyright (C) 2016 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+# Tests for binding Unix domain sockets
+use strict;
+use warnings;
+use Test::More;
+
+foreach my $mod (qw(Plack::Util Plack::Request Plack::Builder Danga::Socket
+                        HTTP::Parser::XS HTTP::Date HTTP::Status)) {
+        eval "require $mod";
+        plan skip_all => "$mod missing for httpd-unix.t" if $@;
+}
+
+use File::Temp qw/tempdir/;
+use IO::Socket::UNIX;
+use Cwd qw/getcwd/;
+use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD :seek);
+my $tmpdir = tempdir('httpd-unix-XXXXXX', TMPDIR => 1, CLEANUP => 1);
+my $unix = "$tmpdir/unix.sock";
+my $httpd = 'blib/script/public-inbox-httpd';
+my $psgi = getcwd() . '/t/httpd-corner.psgi';
+my $out = "$tmpdir/out.log";
+my $err = "$tmpdir/err.log";
+
+my $pid;
+END { kill 'TERM', $pid if defined $pid };
+
+my $spawn_httpd = sub {
+        my (@args) = @_;
+        $pid = fork;
+        if ($pid == 0) {
+                exec $httpd, @args, "--stdout=$out", "--stderr=$err", $psgi;
+                die "FAIL: $!\n";
+        }
+        ok(defined $pid, 'forked httpd process successfully');
+};
+
+ok(!-S $unix, 'UNIX socket does not exist, yet');
+$spawn_httpd->("-l$unix");
+for (1..1000) {
+        last if -S $unix;
+        select undef, undef, undef, 0.02
+}
+
+ok(-S $unix, 'UNIX socket was bound by -httpd');
+sub check_sock ($) {
+        my ($unix) = @_;
+        my $sock = IO::Socket::UNIX->new(Peer => $unix, Type => SOCK_STREAM);
+        ok($sock, 'client UNIX socket connected');
+        ok($sock->write("GET /host-port HTTP/1.0\r\n\r\n"),
+                'wrote req to server');
+        ok($sock->read(my $buf, 4096), 'read response');
+        like($buf, qr!\r\n\r\n127\.0\.0\.1:0\z!,
+                'set REMOTE_ADDR and REMOTE_PORT for Unix socket');
+}
+
+check_sock($unix);
+
+{ # do not clobber existing socket
+        my $fpid = fork;
+        if ($fpid == 0) {
+                open STDOUT, '>>', "$tmpdir/1" or die "redirect failed: $!";
+                open STDERR, '>>', "$tmpdir/2" or die "redirect failed: $!";
+                exec $httpd, '-l', $unix, '-W0', $psgi;
+                die "FAIL: $!\n";
+        }
+        is($fpid, waitpid($fpid, 0), 'second httpd exits');
+        isnt($?, 0, 'httpd failed with failure to bind');
+        open my $fh, "$tmpdir/2" or die "failed to open $tmpdir/2: $!";
+        local $/;
+        my $e = <$fh>;
+        like($e, qr/no listeners bound/i, 'got error message');
+        is(-s "$tmpdir/1", 0, 'stdout was empty');
+}
+
+{
+        my $kpid = $pid;
+        $pid = undef;
+        is(kill('TERM', $kpid), 1, 'terminate existing process');
+        is(waitpid($kpid, 0), $kpid, 'existing httpd terminated');
+        is($?, 0, 'existing httpd exited successfully');
+        ok(-S $unix, 'unix socket still exists');
+}
+{
+        # wait for daemonization
+        $spawn_httpd->("-l$unix", '-D', '-P', "$tmpdir/pid");
+        my $kpid = $pid;
+        $pid = undef;
+        is(waitpid($kpid, 0), $kpid, 'existing httpd terminated');
+        check_sock($unix);
+
+        ok(-f "$tmpdir/pid", 'pid file written');
+        open my $fh, '<', "$tmpdir/pid" or die "open failed: $!";
+        my $rpid = <$fh>;
+        chomp $rpid;
+        like($rpid, qr/\A\d+\z/s, 'pid file looks like a pid');
+        is(kill('TERM', $rpid), 1, 'signalled daemonized process');
+        for (1..100) {
+                kill(0, $rpid) or last;
+                select undef, undef, undef, 0.02;
+        }
+        is(kill(0, $rpid), 0, 'daemonized process exited')
+}
+
+done_testing();