mirror of
https://git.proxmox.com/git/pve-http-server
synced 2025-05-01 14:18:33 +00:00
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:
parent
bda4864145
commit
f2e54bb78a
@ -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) = @_;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user