* [PATCH v2] daemon: support listening on Unix domain sockets
2016-03-03 10:33 [PATCH] daemon: support listening on Unix domain sockets Eric Wong
@ 2016-03-04 0:43 ` Eric Wong
2016-03-05 7:38 ` [PATCH] t/httpd-corner: avoid clobbering existing FDs after fork Eric Wong
0 siblings, 1 reply; 3+ messages in thread
From: Eric Wong @ 2016-03-04 0:43 UTC (permalink / raw)
To: meta
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.
---
v2: extra tests for binding (not just inheriting),
setting a permissive umask, and clobbering stale sockets.
lib/PublicInbox/Daemon.pm | 60 ++++++++++++++++++--------
t/httpd-corner.t | 27 +++++++++++-
t/httpd-unix.t | 105 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 172 insertions(+), 20 deletions(-)
create mode 100644 t/httpd-unix.t
diff --git a/lib/PublicInbox/Daemon.pm b/lib/PublicInbox/Daemon.pm
index c101ecb..9f33c05 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 198a7e9..1956407 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 0000000..580d14d
--- /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();
--
EW
^ permalink raw reply related [flat|nested] 3+ messages in thread