From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: X-Spam-Status: No, score=-2.9 required=3.0 tests=ALL_TRUSTED,AWL,BAYES_00, URIBL_BLOCKED shortcircuit=no autolearn=unavailable version=3.3.2 X-Original-To: meta@public-inbox.org Received: from localhost (dcvr.yhbt.net [127.0.0.1]) by dcvr.yhbt.net (Postfix) with ESMTP id 564F263381A; Fri, 4 Mar 2016 00:43:12 +0000 (UTC) Date: Fri, 4 Mar 2016 00:43:12 +0000 From: Eric Wong To: meta@public-inbox.org Subject: [PATCH v2] daemon: support listening on Unix domain sockets Message-ID: <20160304004312.GA10773@dcvr.yhbt.net> References: <20160303103302.29161-1-e@80x24.org> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <20160303103302.29161-1-e@80x24.org> List-Id: 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 +# 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(); -- EW