From: Eric Wong Date: Thu, 3 Mar 2016 10:33:02 +0000 (+0000) Subject: daemon: support listening on Unix domain sockets X-Git-Tag: v1.0.0~655 X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=8557833d769280495ababfa71f202bf131ea5512;p=public-inbox.git daemon: support listening on Unix domain sockets Listening on Unix domain sockets can be convenient for running behind reverse proxies, avoiding port conflicts, limiting access, or avoiding the overhead (if any) of TCP over loopback. --- 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 +# License: AGPL-3.0+ +# 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();