From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <m.carrara@proxmox.com>
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 C4FDD935DA
 for <pve-devel@lists.proxmox.com>; Mon, 20 Feb 2023 10:59:57 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id A64A824F50
 for <pve-devel@lists.proxmox.com>; Mon, 20 Feb 2023 10:59:57 +0100 (CET)
Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com
 [94.136.29.106])
 (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
 for <pve-devel@lists.proxmox.com>; Mon, 20 Feb 2023 10:59:56 +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 94B0247CBA
 for <pve-devel@lists.proxmox.com>; Mon, 20 Feb 2023 10:59:56 +0100 (CET)
From: Max Carrara <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Date: Mon, 20 Feb 2023 10:59:50 +0100
Message-Id: <20230220095950.16137-1-m.carrara@proxmox.com>
X-Mailer: git-send-email 2.30.2
In-Reply-To: <4472e05c-bac9-17bc-6972-a8788bbef119@proxmox.com>
References: <4472e05c-bac9-17bc-6972-a8788bbef119@proxmox.com>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL 0.017 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
Subject: [pve-devel] [PATCH] http-server: redirect HTTP to HTTPS
X-BeenThere: pve-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe>
X-List-Received-Date: Mon, 20 Feb 2023 09:59:57 -0000

Note: This change isn't final yet and shall only serve as an example.

This change adds another subroutine in the request processing
algorithm that verifies whether the TLS handshake has occurred
after the HTTP header was read, redirecting the user to HTTPS if
it hasn't.

In order to let AnyEvent handle TLS handshakes automatically,
the `tls_autostart` callback is added to the handle's read queue,
replacing the `tls` and `tls_ctx` constructor attributes.

Signed-off-by: Max Carrara <m.carrara@proxmox.com>
---
 src/PVE/APIServer/AnyEvent.pm | 115 ++++++++++++++++++++++++++++++----
 1 file changed, 102 insertions(+), 13 deletions(-)

diff --git a/src/PVE/APIServer/AnyEvent.pm b/src/PVE/APIServer/AnyEvent.pm
index 86f5e07..059d710 100644
--- a/src/PVE/APIServer/AnyEvent.pm
+++ b/src/PVE/APIServer/AnyEvent.pm
@@ -22,6 +22,7 @@ use Compress::Zlib;
 use Digest::MD5;
 use Digest::SHA;
 use Encode;
+use Errno qw(EBADMSG);
 use Fcntl ();
 use Fcntl;
 use File::Find;
@@ -1331,6 +1332,7 @@ sub begin_process_next_request {
 		my @process_steps = \(
 		    &parse_request_header_method,
 		    &parse_request_header_fields,
+                    &ensure_tls_connection,
 		    &postprocess_header_fields,
 		    &authenticate_and_handle_request,
 		);
@@ -1513,6 +1515,36 @@ sub parse_request_header_fields {
     return { success => 1 };
 }
 
+sub ensure_tls_connection {
+    my ($self, $reqstate) = @_;
+
+    $self->dprint("TLS session: " . $reqstate->{hdl}->{tls});
+    $self->dprint("TLS handshake succeeded: " . $reqstate->{tls_handshake_succeeded});
+
+    if ($reqstate->{hdl}->{tls} && $reqstate->{tls_handshake_succeeded}) {
+	return { success => 1 };
+    }
+
+    my $request = $reqstate->{request};
+    my $h_host = $reqstate->{request}->header('Host');
+
+    die "request object not found in reqstate\n"
+	if !$request;
+
+    die "Header field 'Host' not found in request\n"
+	if !$h_host;
+
+    my $secure_host = "https://" . ($h_host =~ s/^http(s)?:\/\///r);
+    my $h_location = $secure_host . $request->uri();
+
+    return {
+	success => 0,
+	http_code => 301,
+	http_message => 'Moved Permanently',
+	http_header => HTTP::Headers->new('Location' => $h_location),
+    };
+}
+
 sub postprocess_header_fields {
     my ($self, $reqstate) = @_;
 
@@ -1950,26 +1982,27 @@ sub accept_connections {
 		timeout => $self->{timeout},
 		linger => 0, # avoid problems with ssh - really needed ?
 		on_eof   => sub {
-		    my ($hdl) = @_;
-		    eval {
-			$self->log_aborted_request($reqstate);
-			$self->client_do_disconnect($reqstate);
-		    };
-		    if (my $err = $@) { syslog('err', $err); }
+		    $self->handle_on_eof($reqstate, @_);
 		},
 		on_error => sub {
-		    my ($hdl, $fatal, $message) = @_;
-		    eval {
-			$self->log_aborted_request($reqstate, $message);
-			$self->client_do_disconnect($reqstate);
-		    };
-		    if (my $err = $@) { syslog('err', "$err"); }
+		    $self->handle_on_error($reqstate, @_);
 		},
-		($self->{tls_ctx} ? (tls => "accept", tls_ctx => $self->{tls_ctx}) : ()));
+		on_starttls => sub {
+		    $self->handle_on_starttls($reqstate, @_);
+		},
+		on_stoptls => sub {
+		    $self->handle_on_stoptls($reqstate, @_);
+		}
+	    );
 	    $handle_creation = 0;
 
 	    $self->dprint("ACCEPT FH" .  $clientfh->fileno() . " CONN$self->{conn_count}");
 
+	    if ($self->{tls_ctx}) {
+		$self->dprint("Setting TLS to autostart");
+		$reqstate->{hdl}->unshift_read(tls_autostart => $self->{tls_ctx}, "accept");
+	    }
+
 	    $self->begin_process_next_request($reqstate);
 	}
     };
@@ -1991,6 +2024,62 @@ sub accept_connections {
     $self->wait_end_loop() if $self->{end_loop};
 }
 
+sub handle_on_eof {
+    my ($self, $reqstate, $handle) = @_;
+
+    eval {
+	$self->log_aborted_request($reqstate);
+	$self->client_do_disconnect($reqstate);
+    };
+
+    if (my $err = $@) { syslog('err', "$err"); }
+}
+
+sub handle_on_error {
+    my ($self, $reqstate, $handle, $fatal, $message) = @_;
+
+    $fatal ||= 0;
+    $message ||= '';
+
+    $self->dprint("Encountered error '$!' - fatal: $fatal; handle: $handle;");
+    $self->dprint("message: $message;");
+
+    eval {
+	if ($!{EBADMSG}) {
+	    $self->error($reqstate, 400, 'bad request');
+	} else {
+	    $self->dprint("Error '$!' not handled!");
+	}
+
+	$self->log_aborted_request($reqstate, $message);
+	$self->client_do_disconnect($reqstate);
+    };
+
+    if (my $err = $@) { syslog('err', "$err"); }
+}
+
+sub handle_on_starttls {
+    my ($self, $reqstate, $handle, $success, $message) = @_;
+
+    eval {
+	$reqstate->{tls_handshake_succeeded} = $success;
+
+	if ($success) {
+	    $self->dprint("TLS handshake for handle $handle successful ($message); session started");
+	} else {
+	    $self->dprint("TLS handshake for handle $handle failed");
+	}
+    };
+
+    if (my $err = $@) { syslog('err', "$err"); }
+}
+
+sub handle_on_stoptls {
+    my ($self, $reqstate, $handle) = @_;
+
+    $self->dprint("TLS session for handle $handle stopped");
+}
+
 # Note: We can't open log file in non-blocking mode and use AnyEvent::Handle,
 # because we write from multiple processes, and that would arbitrarily mix output
 # of all processes.
-- 
2.30.2