From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 4984C60C02 for ; Wed, 18 Nov 2020 11:59:40 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3CD791A7F0 for ; Wed, 18 Nov 2020 11:59:40 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id D7F021A7E1 for ; Wed, 18 Nov 2020 11:59:38 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id A35934393B for ; Wed, 18 Nov 2020 11:59:38 +0100 (CET) From: Dominik Csapak To: pmg-devel@lists.proxmox.com Date: Wed, 18 Nov 2020 11:59:36 +0100 Message-Id: <20201118105937.13079-4-d.csapak@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20201118105937.13079-1-d.csapak@proxmox.com> References: <20201118105937.13079-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.342 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [httpserver.pm, quarantinelink.map, quarantine.pm] Subject: [pmg-devel] [PATCH pmg-api v3 3/3] api2/quarantine: add global sendlink api call X-BeenThere: pmg-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Mail Gateway development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 18 Nov 2020 10:59:40 -0000 this api call takes an email, checks it against the relay domains, and prepares a custom quarantinelink for that email and sends it there this has to happen unauthenticated, since the idea is that the user want to access the quarantine but has no current ticket (and no old spam report with a ticket) we rate limit the requests by allowing only a request per 5 seconds (to prevent dos'ing the internal mail server) and only one request per user/hour this api call is disabled by default if admins want even more ratelimiting, they can setup something like fail2ban to block hosts hitting this api call often Signed-off-by: Dominik Csapak --- src/PMG/API2/Quarantine.pm | 126 +++++++++++++++++++++++++++++++++++++ src/PMG/HTTPServer.pm | 1 + 2 files changed, 127 insertions(+) diff --git a/src/PMG/API2/Quarantine.pm b/src/PMG/API2/Quarantine.pm index 73fb0ec..2085eec 100644 --- a/src/PMG/API2/Quarantine.pm +++ b/src/PMG/API2/Quarantine.pm @@ -8,6 +8,10 @@ use Data::Dumper; use Encode; use File::Path; use IO::File; +use MIME::Entity; +use URI::Escape; +use Time::HiRes qw(usleep gettimeofday tv_interval); +use File::stat (); use Mail::Header; use Mail::SpamAssassin; @@ -195,6 +199,7 @@ __PACKAGE__->register_method ({ { name => 'attachment' }, { name => 'listattachments' }, { name => 'download' }, + { name => 'sendlink' }, ]; return $result; @@ -1239,4 +1244,125 @@ __PACKAGE__->register_method ({ return undef; }}); +my $link_map_fn = "/run/pmgproxy/quarantinelink.map"; +my $per_user_limit = 60*60; # 1 hour + +my sub send_link_mail { + my ($cfg, $receiver) = @_; + + my $hostname = PVE::INotify::nodename(); + my $fqdn = $cfg->get('spamquar', 'hostname') // + PVE::Tools::get_fqdn($hostname); + + my $port = $cfg->get('spamquar', 'port') // 8006; + + my $protocol = $cfg->get('spamquar', 'protocol') // 'https'; + + my $protocol_fqdn_port = "$protocol://$fqdn"; + if (($protocol eq 'https' && $port != 443) || + ($protocol eq 'http' && $port != 80)) { + $protocol_fqdn_port .= ":$port"; + } + + my $mailfrom = $cfg->get ('spamquar', 'mailfrom') // + "Proxmox Mail Gateway "; + + my $ticket = PMG::Ticket::assemble_quarantine_ticket($receiver); + my $esc_ticket = uri_escape($ticket); + my $link = "$protocol_fqdn_port/quarantine?ticket=${esc_ticket}"; + + my $text = "Here is your Link for the Spam Quarantine on $fqdn:\n\n$link\n"; + + my $mail = MIME::Entity->build( + Type => "text/plain", + To => $receiver, + From => $mailfrom, + Subject => "Proxmox Mail Gateway - Quarantine Link", + Data => $text, + ); + + # we use an empty envelope sender (we dont want to receive NDRs) + PMG::Utils::reinject_mail ($mail, '', [$receiver], undef, $fqdn); +} + +__PACKAGE__->register_method ({ + name =>'sendlink', + path => 'sendlink', + method => 'POST', + description => "Send Quarantine link to given e-mail.", + permissions => { user => 'world' }, + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + mail => get_standard_option('pmg-email-address'), + }, + }, + returns => { type => "null" }, + code => sub { + my ($param) = @_; + + my $starttime = [gettimeofday]; + + my $cfg = PMG::Config->new(); + my $is_enabled = $cfg->get('spamquar', 'quarantinelink'); + if (!$is_enabled) { + die "This feature is not enabled\n"; + } + + my $stat = File::stat::stat($link_map_fn); + + if (defined($stat) && ($stat->mtime) + 5 > $starttime->[0]) { + die "Too many requests. Please try again later\n"; + } + + my $domains = PVE::INotify::read_file('domains'); + my $domainregex = PMG::Utils::domain_regex([keys %$domains]); + + my $receiver = $param->{mail}; + + if ($receiver !~ $domainregex) { + return undef; # silently ignore invalid mails + } + + PVE::Tools::lock_file_full("${link_map_fn}.lck", 10, 1, sub { + if (-f $link_map_fn) { + # check if user is allowed to request mail + my $lines = [split("\n", PVE::Tools::file_get_contents($link_map_fn))]; + for my $line (@$lines) { + next if $line !~ m/^\Q$receiver\E (\d+)$/; + if (($1 + $per_user_limit) > $starttime->[0]) { + die "Too many requests for '$receiver', only one request per hour is permitted. ". + "Please try again later\n"; + } else { + last; + } + } + } + }); + die $@ if $@; + + # we are allowed to send mail, lock and update file and send + PVE::Tools::lock_file("${link_map_fn}.lck", 10, sub { + my $newdata = ""; + if (-f $link_map_fn) { + my $data = PVE::Tools::file_get_contents($link_map_fn); + for my $line (split("\n", $data)) { + if ($line =~ m/^(.*) (\d+)$/) { + if (($2 + $per_user_limit) > $starttime->[0]) { + $newdata .= $line . "\n"; + } + } + } + } + $newdata .= "$receiver $starttime->[0]\n"; + PVE::Tools::file_set_contents($link_map_fn, $newdata); + }); + die $@ if $@; + + send_link_mail($cfg, $receiver); + + return undef; + }}); + 1; diff --git a/src/PMG/HTTPServer.pm b/src/PMG/HTTPServer.pm index eb48b5f..3dc9655 100755 --- a/src/PMG/HTTPServer.pm +++ b/src/PMG/HTTPServer.pm @@ -58,6 +58,7 @@ sub auth_handler { # explicitly allow some calls without auth if (($rel_uri eq '/access/domains' && $method eq 'GET') || + ($rel_uri eq '/quarantine/sendlink' && ($method eq 'GET' || $method eq 'POST')) || ($rel_uri eq '/access/ticket' && ($method eq 'GET' || $method eq 'POST'))) { $require_auth = 0; } -- 2.20.1