#!/usr/bin/perl # Copyright (C) 2020 Marius Schellenberger use strict; use warnings; use Config; use IO::Socket; use IO::Select; use Sys::Syslog qw(:standard :macros); use IPC::Run qw(start finish); # Configs # File to store the user:password hash mappings my $authfile = "/etc/mail/secrets"; # UNIX Domain Socket inside the web chroot my $sfile = "/var/www/run/smtp_auth_sync.sock"; # Absolute path of smtpctl my $smtpctl = "/usr/sbin/smtpctl"; # Group OpenSMTPD runs with my $smtp_group = "_smtpd"; # Group the webserver runs with my $www_group = "www"; sub read_timeout { my ($s, $conn) = @_; $s->can_read(2) or return 0; return readline($conn); } my ($r_uid,$r_gid) = (getpwnam("root"))[2,3]; my $s_gid = (getpwnam($smtp_group))[3] or die "group '${smtp_group}' not found: $!"; my $w_gid = (getpwnam($www_group))[3] or die "group '${www_group}' not found: $!"; my $authfiletmp = "${authfile}.tmp"; if ($Config{osname} eq "openbsd") { eval "use OpenBSD::Pledge"; if ($Config{osvers} >= "6.6") { eval "use OpenBSD::Unveil"; unveil($authfile, "rwc") or die "Unable to unveil: $!"; unveil($authfiletmp, "rwc") or die "Unable to unveil: $!"; unveil($sfile, "rwc") or die "Unable to unveil: $!"; unveil($smtpctl, "rx") or die "Unable to unveil: $!"; } pledge("rpath wpath cpath fattr chown unix proc exec prot_exec") or die "Unable to pledge: $!"; } openlog("smtp_auth_sync", "ndelay,pid", LOG_AUDIT) or die "Could not open syslog: $!"; if (-e $sfile) { unlink $sfile or die "Could not delete socket file '${sfile}': $!"; } my $sock = IO::Socket::UNIX->new( Type => SOCK_STREAM(), Local => $sfile, Listen => 1, ) or die "Could not create socket: '$!'"; chown($r_uid, $w_gid, $sfile); chmod(0620, $sfile); sub cleanup { $sock->close(); unlink $sfile; exit 0; } $SIG{INT} = \&cleanup; $SIG{TERM} = \&cleanup; my @enc = (${smtpctl}, "encrypt"); my @update = (${smtpctl}, "update", "table", "secrets"); while (my $conn = $sock->accept()) { my $user; my $pass; my $s = IO::Select->new($conn); $user = read_timeout($s, $conn) or do { $conn->close(); next; }; chomp($user); $pass = read_timeout($s, $conn) or do { $conn->close(); next; }; chomp($pass); $conn->close(); if ($user eq "" or $pass eq "") { next; } my $hash; my $h = start(\@enc, \<", $authfile) or next; chown($r_uid, $s_gid, $authfile); chmod(0640, $authfile); print $auth "${user} ${hash}\n"; close($auth); next; } my $found = 0; open(my $auth, "<", $authfile) or do { print "Could not open file '${authfile}': $!"; next; }; chown($r_uid, $s_gid, $authfile); chmod(0640, $authfile); open(my $authtmp, ">", $authfiletmp) or do { print "Could not open file '${authfiletmp}': $!"; next; }; chown($r_uid, $s_gid, $authfiletmp); chmod(0640, $authfiletmp); while (<$auth>) { my $line = $_; if ($line =~ /$user/) { print $authtmp "${user} ${hash}\n"; $found = 1; } else { print $authtmp "${line}"; } } if ($found == 0) { print $authtmp "${user} ${hash}\n"; } close($auth); close($authtmp); rename($authfiletmp, $authfile); my $out; my $u = start(\@update, \undef, \$out); finish $u or next; syslog(LOG_INFO, "updated password of user ${user}"); }