commit d4dba07d4472442285f5c872898d61dff141cf47
parent 27b6c938f705f32cdfb04e82c50bf165b2076bac
Author: Kyle Milz <kyle@getaddrinfo.net>
Date:   Mon, 27 Apr 2015 21:36:41 -0600
pc_fcgi: try to implement privsep properly
- require root:daemon permissions for stuff in /var/www/{db,run}
- open fcgi socket, db handle, syslog all as root
- fork, then chroot in child and serve connections from there
- parent waits to clean up everything afterwards
Diffstat:
2 files changed, 60 insertions(+), 49 deletions(-)
diff --git a/openbsd_rc.d_pc_fcgi b/openbsd_rc.d_pc_fcgi
@@ -4,4 +4,6 @@ daemon="perl /home/kyle/src/pricechart/pc_fcgi"
 
 . /etc/rc.d/rc.subr
 
+pexp="perl: ps_fcgi sloth"
+
 rc_cmd $1
diff --git a/pc_fcgi b/pc_fcgi
@@ -3,7 +3,7 @@
 use strict;
 use warnings;
 
-# because we chroot all dependencies must be explicitly listed
+# all dependencies explicitly listed
 use Config::Grammar;
 use DBD::SQLite;
 use Encode;
@@ -26,61 +26,80 @@ getopts("v", \%args);
 
 # fork into background unless verbose
 unless ($args{v}) {
-	exit if (fork());
+	if (fork()) {
+		exit();
+	}
 }
 
 my $cfg = get_config();
 my %http_cfg = %{$cfg->{"http"}};
 
-# translates user/group names to id's, needs to be before chroot
-my $uid_name = $http_cfg{"uid"};
-my $gid_name = $http_cfg{"gid"};
-my $uid = getpwnam($uid_name) or die "error: user $uid_name does not exist\n";
-my $gid = getgrnam($gid_name) or die "error: group $gid_name does not exist\n";
-print "info: $uid_name:$gid_name -> $uid:$gid\n" if ($args{v});;
-
-# chroot early
-print "info: chrooting to $http_cfg{chroot}\n" if ($args{v});
-chroot($http_cfg{"chroot"});
-chdir("/");
-
-# XXX: verify we have indeed dropped privileges?
-$( = $) = "$gid $gid";
-$< = $> = $uid;
-print "info: uid:gid set to $<:$(\n" if ($args{v});
-
-print "info: opening syslog\n" if ($args{v});
-openlog("pc_fcgi", LOG_PID, LOG_DAEMON);
+openlog("ps_fcgi", LOG_PID, LOG_DAEMON);
 
 if (-e $http_cfg{socket}) {
-	my $msg = "socket $http_cfg{socket} exists, not starting\n";
-	print "error: $msg\n" if ($args{v});
+	my $msg = "error: socket $http_cfg{socket} exists\n";
+	print "$msg\n" if ($args{v});
 	syslog(LOG_ERR, $msg);
 	exit;
 }
 
-# XXX: i need to be sudo for this to work? after we've dropped privileges?
-print "info: opening $http_cfg{socket}\n" if ($args{v});
 my $socket = FCGI::OpenSocket($http_cfg{socket}, 1024);
-
-my $dbh = get_dbh($cfg->{"http"}, $http_cfg{db_dir}, $args{v});
 my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket,
 	FCGI::FAIL_ACCEPT_ON_INTR);
 
-print "info: making template, include_path = $http_cfg{htdocs}\n" if ($args{v});
+my $dbh = get_dbh($cfg->{http}, $http_cfg{db_dir});
+my $sql = qq{select distinct manufacturer, part_num from prices where
+	manufacturer like ? or part_num like ?};
+my $srch_sth = $dbh->prepare($sql);
+
+my ($user, $group) = ($http_cfg{uid}, $http_cfg{gid});
+my $uid = getpwnam($user)  or die "error: user $user does not exist\n";
+my $gid = getgrnam($group) or die "error: group $group does not exist\n";
+chown $uid, $gid, $http_cfg{socket} or die "error: chown $uid:$gid: $!";
+
+if (fork()) {
+	# parent
+	$0 = "ps_fcgi [priv]";
+
+	# child should catch sigint and exit nicely, then we exit nicely here too
+	$SIG{INT} = "IGNORE";
+
+	print "info: parent: alive\n" if ($args{v});
+	syslog(LOG_INFO, "parent: alive");
+	wait();
+
+	print "info: parent: cleaning up\n" if ($args{v});
+	syslog(LOG_INFO, "parent: shutdown");
+
+	$dbh->disconnect();
+	FCGI::CloseSocket($socket);
+	unlink($http_cfg{socket}) or print "error: could not unlink $http_cfg{socket}: $!";
+	closelog();
+
+	exit 0;
+}
+
+# child
+$0 = "ps_fcgi sloth";
+
+print "info: child: chroot $http_cfg{chroot}\n" if ($args{v});
+chroot($http_cfg{"chroot"});
+chdir("/");
+
+$( = $) = "$gid $gid";
+$< = $> = $uid;
+print "info: child: uid:gid appears to be $<:$(\n" if ($args{v});
+
 my $config = { INCLUDE_PATH => "$http_cfg{htdocs}/tt" };
 my $template = Template->new($config) || die $Template::ERROR . "\n";
 
-# try hard here not to use EVAL_PERL in the template
-my $sql = "select distinct manufacturer, part_num " .
-	"from prices where part_num like ? or manufacturer like ?";
-my $search_sth = $dbh->prepare($sql);
-
 # intercept signals to shut down cleanly
 $SIG{INT} =  \&child_sig;
 $SIG{TERM} = \&child_sig;
 
-syslog(LOG_INFO, "startup");
+syslog(LOG_INFO, "child: ready");
+print "info: child: ready\n" if ($args{v});
+
 my $requests_total = 0;
 while ($request->Accept() >= 0) {
 	print "Content-Type: text/html\r\n\r\n";
@@ -90,27 +109,17 @@ while ($request->Accept() >= 0) {
 	$input = uri_unescape($input);
 
 	# fuzzy search on manufacturer and part number
-	$search_sth->execute("%$input%", "%$input%");
-	my $products = $search_sth->fetchall_arrayref();
-
-	my $vars = { query => $input, results => $products };
+	$srch_sth->execute("%$input%", "%$input%");
+	my $vars = { query => $input, results => $srch_sth->fetchall_arrayref() };
 	$template->process("search.tt", $vars)
 		|| print "template: " . $template->error();
+
 	$requests_total++;
 }
-syslog(LOG_INFO, "shutdown, $requests_total total requests served");
-closelog();
-
-$search_sth = undef;
-$dbh->disconnect();
-
-FCGI::CloseSocket($socket);
-unlink($http_cfg{socket}) or print "error: could not unlink $http_cfg{socket}: $!";
+syslog(LOG_INFO, "child: shutdown, $requests_total requests");
 
 sub child_sig
 {
-	my $signame = shift;
-
 	$request->LastCall();
-	print "info: caught SIG$signame\n" if ($args{v});
+	print "info: child: caught signal " . lc shift . "\n" if ($args{v});
 }