From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: 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 B270E85A2 for ; Fri, 3 Mar 2023 18:30:04 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8C4A81A40E for ; 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 ; 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 C34AF46456 for ; Fri, 3 Mar 2023 18:30:02 +0100 (CET) From: Max Carrara To: pve-devel@lists.proxmox.com Date: Fri, 3 Mar 2023 18:29:49 +0100 Message-Id: <20230303172951.197711-3-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.038 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 2/4] anyevent: move auth and request handling into separate subroutine X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 03 Mar 2023 17:30:04 -0000 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 --- 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