package PMG::QuarantineHTTPServer;

use strict;
use warnings;

use HTTP::Status qw(:constants);

use PVE::Exception qw(raise);

use base('PMG::HTTPServer');

# API surface reachable through the dedicated user quarantine listener.
#
# This listener is meant to be exposed to untrusted networks, unlike pmgproxy,
# which serves the full management API on the same socket as the quarantine.
# Everything not matched here is refused before authentication happens, so a
# stolen or forged quarantine ticket cannot reach node, config or access
# management endpoints.
#
# The list was derived from the ExtJS views in pmg-gui, from the compiled
# pmg-mobile-quarantine-ui (whose source Proxmox does not publish), and checked
# against eleven months of pmgproxy request logs on a production node. Every
# quarantine call observed there is covered by the first entry.
#
# Note that $rel_uri has the /api2/<format> prefix already stripped, so these
# patterns cover the json, extjs and htmlmail formatters at once.
#
# Beware when extending this from the mobile UI: its wasm string table also
# contains dead paths from a shared Rust crate written for PVE and PBS, such as
# /access/domains, /access/acl and /access/openid/*. PMG registers none of
# those -- it uses /access/auth-realm and /access/oidc. Confirm an endpoint
# exists in PMG::API2 before allowing it.
my $URI_ALLOWLIST = [
    # The quarantine API itself: spam, virus, attachment, content,
    # listattachments, welcomelist, blocklist, whitelist, quarusers, spamusers,
    # sendlink and link. POST is needed for deliver and for list edits; the
    # quarantine API enforces the quser role itself.
    { re => qr{^/quarantine(?:/.*)?$}, methods => undef },

    # Login. OIDC is used by the ExtJS LoginView.
    { re => qr{^/access/ticket$}, methods => { GET => 1, POST => 1 } },
    { re => qr{^/access/oidc/(?:auth-url|login)$}, methods => { POST => 1 } },

    # Realm discovery for authmode ldap and ldapticket.
    { re => qr{^/access/auth-realm$}, methods => { GET => 1 } },

    # Version banner.
    { re => qr{^/version$}, methods => { GET => 1 } },

    # The mobile UI reads the subscription status to decide whether to show its
    # banner. Deliberately pinned to this single read-only endpoint rather than
    # opening up /nodes.
    { re => qr{^/nodes/[^/]+/subscription$}, methods => { GET => 1 } },
];

sub uri_allowed {
    my ($method, $rel_uri) = @_;

    for my $entry ($URI_ALLOWLIST->@*) {
        next if $rel_uri !~ $entry->{re};
        return 1 if !defined($entry->{methods});
        return 1 if $entry->{methods}->{$method};
    }

    return 0;
}

sub auth_handler {
    my ($self, $method, $rel_uri, $ticket, $token, $api_token, $peer_host) = @_;

    if (!uri_allowed($method, $rel_uri)) {
        raise(
            "'$rel_uri' is not available on the quarantine listener\n",
            code => HTTP_FORBIDDEN,
        );
    }

    return $self->SUPER::auth_handler(
        $method, $rel_uri, $ticket, $token, $api_token, $peer_host,
    );
}

1;
