]> git.proxmox.com Git - pmg-api.git/commitdiff
api2/quarantine: add global sendlink api call
authorDominik Csapak <d.csapak@proxmox.com>
Wed, 18 Nov 2020 10:59:36 +0000 (11:59 +0100)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 18 Nov 2020 16:04:38 +0000 (17:04 +0100)
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
src/PMG/HTTPServer.pm

index 750afbe199de77203fb720a67f3ffd4bf6327f94..bb776d4ca1c37e445a8b80ee72d8d1945a6243d1 100644 (file)
@@ -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;
@@ -193,6 +197,7 @@ __PACKAGE__->register_method ({
            { name => 'attachment' },
            { name => 'listattachments' },
            { name => 'download' },
+           { name => 'sendlink' },
        ];
 
        return $result;
@@ -1237,4 +1242,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;
index eb48b5f97f9a69f5bce27c32758d6f1ba1cb0922..3dc9655e662c6e249c033ba9159f0955f14bddad 100755 (executable)
@@ -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;
     }