public inbox for pmg-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH pmg-api v3 3/3] api2/quarantine: add global sendlink api call
Date: Wed, 18 Nov 2020 11:59:36 +0100	[thread overview]
Message-ID: <20201118105937.13079-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20201118105937.13079-1-d.csapak@proxmox.com>

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 <d.csapak@proxmox.com>
---
 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 <postmaster>";
+
+    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





  parent reply	other threads:[~2020-11-18 10:59 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-11-18 10:59 [pmg-devel] [PATCH pmg-api/gui v3] add quarantine self service button Dominik Csapak
2020-11-18 10:59 ` [pmg-devel] [PATCH pmg-api v3 1/3] refactor domain_regex to Utils Dominik Csapak
2020-11-18 10:59 ` [pmg-devel] [PATCH pmg-api v3 2/3] add 'quarantinelink' to spamquar config Dominik Csapak
2020-11-18 10:59 ` Dominik Csapak [this message]
2020-11-18 10:59 ` [pmg-devel] [PATCH pmg-gui v3 1/1] add 'Request Quarantine Link' Button to LoginView Dominik Csapak
2020-11-18 16:56 ` [pmg-devel] applied: [PATCH pmg-api/gui v3] add quarantine self service button Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20201118105937.13079-4-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pmg-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal