From: Max Carrara <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v2 http-server 2/4] anyevent: move auth and request handling into separate subroutine
Date: Fri, 3 Mar 2023 18:29:49 +0100 [thread overview]
Message-ID: <20230303172951.197711-3-m.carrara@proxmox.com> (raw)
In-Reply-To: <20230303172951.197711-1-m.carrara@proxmox.com>
The part responsible for authentication and subsequent request
handling is moved into the new `authenticate_and_handle_request`
subroutine.
If `authenticate_and_handle_request` doesn't return early, it returns
`1` for further control flow purposes.
Some minor things are formatted or renamed for readability's sake.
Signed-off-by: Max Carrara <m.carrara@proxmox.com>
---
src/PVE/APIServer/AnyEvent.pm | 324 +++++++++++++++++++---------------
1 file changed, 180 insertions(+), 144 deletions(-)
diff --git a/src/PVE/APIServer/AnyEvent.pm b/src/PVE/APIServer/AnyEvent.pm
index 294c035..636502b 100644
--- a/src/PVE/APIServer/AnyEvent.pm
+++ b/src/PVE/APIServer/AnyEvent.pm
@@ -1314,156 +1314,13 @@ sub unshift_read_header {
my $r = $reqstate->{request};
if ($line eq '') {
- my $path = uri_unescape($r->uri->path());
- my $method = $r->method();
-
$r->push_header($state->{key}, $state->{val})
if $state->{key};
- my $base_uri = $self->{base_uri};
-
- my $len = $r->header('Content-Length');
- my $host_header = $r->header('Host');
-
$self->process_header($reqstate) or return;
# header processing complete - authenticate now
+ $self->authenticate_and_handle_request($reqstate) or return;
- my $auth = {};
- if ($self->{spiceproxy}) {
- my $connect_str = $host_header;
- my ($vmid, $node, $port) = $self->verify_spice_connect_url($connect_str);
- if (!(defined($vmid) && $node && $port)) {
- $self->error($reqstate, HTTP_UNAUTHORIZED, "invalid ticket");
- return;
- }
- $self->handle_spice_proxy_request($reqstate, $connect_str, $vmid, $node, $port);
- return;
- } elsif ($path =~ m/^\Q$base_uri\E/) {
- my $token = $r->header('CSRFPreventionToken');
- my $cookie = $r->header('Cookie');
- my $auth_header = $r->header('Authorization');
-
- # prefer actual cookie
- my $ticket = PVE::APIServer::Formatter::extract_auth_value($cookie, $self->{cookie_name});
-
- # fallback to cookie in 'Authorization' header
- $ticket = PVE::APIServer::Formatter::extract_auth_value($auth_header, $self->{cookie_name})
- if !$ticket;
-
- # finally, fallback to API token if no ticket has been provided so far
- my $api_token;
- $api_token = PVE::APIServer::Formatter::extract_auth_value($auth_header, $self->{apitoken_name})
- if !$ticket;
-
- my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri});
- if (!$format) {
- $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri");
- return;
- }
-
- eval {
- $auth = $self->auth_handler($method, $rel_uri, $ticket, $token, $api_token,
- $reqstate->{peer_host});
- };
- if (my $err = $@) {
- # HACK: see Note 1
- Net::SSLeay::ERR_clear_error();
- # always delay unauthorized calls by 3 seconds
- my $delay = 3;
-
- if (ref($err) eq "PVE::Exception") {
-
- $err->{code} ||= HTTP_INTERNAL_SERVER_ERROR,
- my $resp = HTTP::Response->new($err->{code}, $err->{msg});
- $self->response($reqstate, $resp, undef, 0, $delay);
-
- } elsif (my $formatter = PVE::APIServer::Formatter::get_login_formatter($format)) {
- my ($raw, $ct, $nocomp) =
- $formatter->($path, $auth, $self->{formatter_config});
- my $resp;
- if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
- $resp = $raw;
- } else {
- $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, "Login Required");
- $resp->header("Content-Type" => $ct);
- $resp->content($raw);
- }
- $self->response($reqstate, $resp, undef, $nocomp, $delay);
- } else {
- my $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, $err);
- $self->response($reqstate, $resp, undef, 0, $delay);
- }
- return;
- }
- }
-
- $reqstate->{log}->{userid} = $auth->{userid};
-
- if ($len) {
-
- if (!($method eq 'PUT' || $method eq 'POST')) {
- $self->error($reqstate, 501, "Unexpected content for method '$method'");
- return;
- }
-
- my $ctype = $r->header('Content-Type');
- my ($ct, $boundary) = $ctype ? parse_content_type($ctype) : ();
-
- if ($auth->{isUpload} && !$self->{trusted_env}) {
- die "upload 'Content-Type '$ctype' not implemented\n"
- if !($boundary && $ct && ($ct eq 'multipart/form-data'));
-
- die "upload without content length header not supported" if !$len;
-
- die "upload without content length header not supported" if !$len;
-
- $self->dprint("start upload $path $ct $boundary");
-
- my $tmpfilename = get_upload_filename();
- my $outfh = IO::File->new($tmpfilename, O_RDWR|O_CREAT|O_EXCL, 0600) ||
- die "unable to create temporary upload file '$tmpfilename'";
-
- $reqstate->{keep_alive} = 0;
-
- my $boundlen = length($boundary) + 8; # \015?\012--$boundary--\015?\012
-
- my $state = {
- size => $len,
- boundary => $boundary,
- ctx => Digest::MD5->new,
- boundlen => $boundlen,
- maxheader => 2048 + $boundlen, # should be large enough
- params => decode_urlencoded($r->url->query()),
- phase => 0,
- read => 0,
- post_size => 0,
- starttime => [gettimeofday],
- outfh => $outfh,
- };
- $reqstate->{tmpfilename} = $tmpfilename;
- $reqstate->{hdl}->on_read(sub {
- $self->file_upload_multipart($reqstate, $auth, $method, $path, $state);
- });
- return;
- }
-
- if ($len > $limit_max_post) {
- $self->error($reqstate, 501, "for data too large");
- return;
- }
-
- if (!$ct || $ct eq 'application/x-www-form-urlencoded' || $ct eq 'application/json') {
- $reqstate->{hdl}->unshift_read(chunk => $len, sub {
- my ($hdl, $data) = @_;
- $r->content($data);
- $self->handle_request($reqstate, $auth, $method, $path);
- });
- } else {
- $self->error($reqstate, 506, "upload 'Content-Type '$ctype' not implemented");
- }
- } else {
- $self->handle_request($reqstate, $auth, $method, $path);
- }
} elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) {
$r->push_header($state->{key}, $state->{val}) if $state->{key};
($state->{key}, $state->{val}) = ($1, $2);
@@ -1531,6 +1388,185 @@ sub process_header {
return 1;
}
+sub authenticate_and_handle_request {
+ my ($self, $reqstate) = @_;
+
+ my $request = $reqstate->{request};
+ my $method = $request->method();
+
+ my $path = uri_unescape($request->uri->path());
+ my $base_uri = $self->{base_uri};
+
+ my $auth = {};
+
+ if ($self->{spiceproxy}) {
+ my $connect_str = $request->header('Host');
+ my ($vmid, $node, $port) = $self->verify_spice_connect_url($connect_str);
+
+ if (!(defined($vmid) && $node && $port)) {
+ $self->error($reqstate, HTTP_UNAUTHORIZED, "invalid ticket");
+ return;
+ }
+
+ $self->handle_spice_proxy_request($reqstate, $connect_str, $vmid, $node, $port);
+ return;
+
+ } elsif ($path =~ m/^\Q$base_uri\E/) {
+ my $token = $request->header('CSRFPreventionToken');
+ my $cookie = $request->header('Cookie');
+ my $auth_header = $request->header('Authorization');
+
+ # prefer actual cookie
+ my $ticket = PVE::APIServer::Formatter::extract_auth_value(
+ $cookie,
+ $self->{cookie_name}
+ );
+
+ # fallback to cookie in 'Authorization' header
+ if (!$ticket) {
+ $ticket = PVE::APIServer::Formatter::extract_auth_value(
+ $auth_header,
+ $self->{cookie_name}
+ );
+ }
+
+ # finally, fallback to API token if no ticket has been provided so far
+ my $api_token;
+ if (!$ticket) {
+ $api_token = PVE::APIServer::Formatter::extract_auth_value(
+ $auth_header,
+ $self->{apitoken_name}
+ );
+ }
+
+ my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri});
+ if (!$format) {
+ $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri");
+ return;
+ }
+
+ eval {
+ $auth = $self->auth_handler(
+ $method,
+ $rel_uri,
+ $ticket,
+ $token,
+ $api_token,
+ $reqstate->{peer_host}
+ );
+ };
+ if (my $err = $@) {
+ # HACK: see Note 1
+ Net::SSLeay::ERR_clear_error();
+ # always delay unauthorized calls by 3 seconds
+ my $delay = 3;
+
+ if (ref($err) eq "PVE::Exception") {
+
+ $err->{code} ||= HTTP_INTERNAL_SERVER_ERROR,
+ my $resp = HTTP::Response->new($err->{code}, $err->{msg});
+ $self->response($reqstate, $resp, undef, 0, $delay);
+
+ } elsif (my $formatter = PVE::APIServer::Formatter::get_login_formatter($format)) {
+ my ($raw, $ct, $nocomp) =
+ $formatter->($path, $auth, $self->{formatter_config});
+
+ my $resp;
+ if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
+ $resp = $raw;
+
+ } else {
+ $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, "Login Required");
+ $resp->header("Content-Type" => $ct);
+ $resp->content($raw);
+ }
+
+ $self->response($reqstate, $resp, undef, $nocomp, $delay);
+
+ } else {
+ my $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, $err);
+ $self->response($reqstate, $resp, undef, 0, $delay);
+ }
+
+ return;
+ }
+ }
+
+ $reqstate->{log}->{userid} = $auth->{userid};
+ my $len = $request->header('Content-Length');
+
+ if ($len) {
+
+ if (!($method eq 'PUT' || $method eq 'POST')) {
+ $self->error($reqstate, 501, "Unexpected content for method '$method'");
+ return;
+ }
+
+ my $ctype = $request->header('Content-Type');
+ my ($ct, $boundary) = $ctype ? parse_content_type($ctype) : ();
+
+ if ($auth->{isUpload} && !$self->{trusted_env}) {
+ die "upload 'Content-Type '$ctype' not implemented\n"
+ if !($boundary && $ct && ($ct eq 'multipart/form-data'));
+
+ die "upload without content length header not supported" if !$len;
+
+ die "upload without content length header not supported" if !$len;
+
+ $self->dprint("start upload $path $ct $boundary");
+
+ my $tmpfilename = get_upload_filename();
+ my $outfh = IO::File->new($tmpfilename, O_RDWR|O_CREAT|O_EXCL, 0600) ||
+ die "unable to create temporary upload file '$tmpfilename'";
+
+ $reqstate->{keep_alive} = 0;
+
+ my $boundlen = length($boundary) + 8; # \015?\012--$boundary--\015?\012
+
+ my $state = {
+ size => $len,
+ boundary => $boundary,
+ ctx => Digest::MD5->new,
+ boundlen => $boundlen,
+ maxheader => 2048 + $boundlen, # should be large enough
+ params => decode_urlencoded($request->url->query()),
+ phase => 0,
+ read => 0,
+ post_size => 0,
+ starttime => [gettimeofday],
+ outfh => $outfh,
+ };
+ $reqstate->{tmpfilename} = $tmpfilename;
+ $reqstate->{hdl}->on_read(sub {
+ $self->file_upload_multipart($reqstate, $auth, $method, $path, $state);
+ });
+
+ return;
+ }
+
+ if ($len > $limit_max_post) {
+ $self->error($reqstate, 501, "for data too large");
+ return;
+ }
+
+ if (!$ct || $ct eq 'application/x-www-form-urlencoded' || $ct eq 'application/json') {
+ $reqstate->{hdl}->unshift_read(chunk => $len, sub {
+ my ($hdl, $data) = @_;
+ $request->content($data);
+ $self->handle_request($reqstate, $auth, $method, $path);
+ });
+
+ } else {
+ $self->error($reqstate, 506, "upload 'Content-Type '$ctype' not implemented");
+ }
+
+ } else {
+ $self->handle_request($reqstate, $auth, $method, $path);
+ }
+
+ return 1;
+}
+
sub push_request_header {
my ($self, $reqstate) = @_;
--
2.30.2
next prev parent reply other threads:[~2023-03-03 17:30 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-03-03 17:29 [pve-devel] [PATCH v2 http-server 0/4] refactor HTTP request processing Max Carrara
2023-03-03 17:29 ` [pve-devel] [PATCH v2 http-server 1/4] anyevent: move header processing into separate subroutine Max Carrara
2023-03-03 17:29 ` Max Carrara [this message]
2023-03-03 17:29 ` [pve-devel] [PATCH v2 http-server 3/4] fix #4494: anyevent: redirect HTTP to HTTPS Max Carrara
2023-03-03 17:29 ` [pve-devel] [PATCH v2 http-server 4/4] anyevent: fix whitespace Max Carrara
2023-03-07 10:20 ` [pve-devel] applied series: [PATCH v2 http-server 0/4] refactor HTTP request processing Fabian Grünbichler
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=20230303172951.197711-3-m.carrara@proxmox.com \
--to=m.carrara@proxmox.com \
--cc=pve-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