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 38FF185B3
 for <pve-devel@lists.proxmox.com>; Fri,  3 Mar 2023 18:30:34 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id 1A0D91A3F2
 for <pve-devel@lists.proxmox.com>; Fri,  3 Mar 2023 18:30:04 +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>; Fri,  3 Mar 2023 18:30:03 +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 BB34A41D6F
 for <pve-devel@lists.proxmox.com>; Fri,  3 Mar 2023 18:30:02 +0100 (CET)
From: Max Carrara <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Date: Fri,  3 Mar 2023 18:29:50 +0100
Message-Id: <20230303172951.197711-4-m.carrara@proxmox.com>
X-Mailer: git-send-email 2.30.2
In-Reply-To: <20230303172951.197711-1-m.carrara@proxmox.com>
References: <20230303172951.197711-1-m.carrara@proxmox.com>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL 0.041 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 v2 http-server 3/4] fix #4494: anyevent:
 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: Fri, 03 Mar 2023 17:30:34 -0000

Allow HTTP connections up until the request's header has been
parsed and processed. If no TLS handshake has been completed
beforehand, the server now responds with either a
'301 Moved Permanently' or a '308 Permanent Redirect' as noted in the
MDN web docs[1].

This is done after the header was parsed; for the redirect to work,
the `Host` header field of the request is used to create the
`Location` field of the response. This makes redirections independent
of how the server is accessed (e.g. via IP, localhost, FQDN, ...)
possible.

Upon redirection the client is immediately disconnected; otherwise,
they would have to wait for the connection to time out until
they may reconnect via TLS again.

[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301

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

diff --git a/src/PVE/APIServer/AnyEvent.pm b/src/PVE/APIServer/AnyEvent.pm
index 636502b..d1bba3c 100644
--- a/src/PVE/APIServer/AnyEvent.pm
+++ b/src/PVE/APIServer/AnyEvent.pm
@@ -1318,7 +1318,7 @@ sub unshift_read_header {
 		    if $state->{key};
 
 		$self->process_header($reqstate) or return;
-		# header processing complete - authenticate now
+		$self->ensure_tls_connection($reqstate) or return;
 		$self->authenticate_and_handle_request($reqstate) or return;
 
 	    } elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) {
@@ -1388,6 +1388,43 @@ sub process_header {
     return 1;
 }
 
+sub ensure_tls_connection {
+    my ($self, $reqstate) = @_;
+
+    # Skip if server doesn't use TLS
+    if (!$self->{tls_ctx}) {
+	return 1;
+    }
+
+    # TLS session exists, so the handshake has succeeded
+    if ($reqstate->{hdl}->{tls}) {
+	return 1;
+    }
+
+    my $request = $reqstate->{request};
+    my $method = $request->method();
+
+    my $h_host = $reqstate->{request}->header('Host');
+
+    die "Header field 'Host' not found in request\n"
+	if !$h_host;
+
+    my $secure_host = "https://" . ($h_host =~ s/^http(s)?:\/\///r);
+
+    my $header = HTTP::Headers->new('Location' => $secure_host . $request->uri());
+
+    if ($method eq 'GET' || $method eq 'HEAD') {
+	$self->error($reqstate, 301, 'Moved Permanently', $header);
+    } else {
+	$self->error($reqstate, 308, 'Permanent Redirect', $header);
+    }
+
+    # disconnect the client so they may immediately connect again via HTTPS
+    $self->client_do_disconnect($reqstate);
+
+    return;
+}
+
 sub authenticate_and_handle_request {
     my ($self, $reqstate) = @_;
 
@@ -1795,11 +1832,16 @@ sub accept_connections {
 		    };
 		    if (my $err = $@) { syslog('err', "$err"); }
 		},
-		($self->{tls_ctx} ? (tls => "accept", tls_ctx => $self->{tls_ctx}) : ()));
+	    );
 	    $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->push_request_header($reqstate);
 	}
     };
-- 
2.30.2