header processing: factor out auth and request handling

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>
This commit is contained in:
Max Carrara 2023-03-03 18:29:49 +01:00 committed by Fabian Grünbichler
parent bda4864145
commit f2e54bb78a

View File

@ -1310,156 +1310,13 @@ sub unshift_read_header {
my $r = $reqstate->{request}; my $r = $reqstate->{request};
if ($line eq '') { if ($line eq '') {
my $path = uri_unescape($r->uri->path());
my $method = $r->method();
$r->push_header($state->{key}, $state->{val}) $r->push_header($state->{key}, $state->{val})
if $state->{key}; 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; $self->process_header($reqstate) or return;
# header processing complete - authenticate now # 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*(.*)/) { } elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) {
$r->push_header($state->{key}, $state->{val}) if $state->{key}; $r->push_header($state->{key}, $state->{val}) if $state->{key};
($state->{key}, $state->{val}) = ($1, $2); ($state->{key}, $state->{val}) = ($1, $2);
@ -1527,6 +1384,185 @@ sub process_header {
return 1; 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 { sub push_request_header {
my ($self, $reqstate) = @_; my ($self, $reqstate) = @_;