5 use Digest
::SHA1
qw(sha1_base64);
16 use HTTP
::Request
::Common
;
17 use HTTP
::Status
qw(:constants :is status_message);
20 use PVE
::AccessControl
;
21 use PVE
::RPCEnvironment
;
23 use Data
::Dumper
; # fixme: remove
25 my $cookie_name = 'PVEAuthCookie';
27 my $baseuri = "/api2";
29 # http://perl.apache.org/docs/2.0/api/Apache2/SubProcess.html
31 sub extract_auth_cookie
{
34 return undef if !$cookie;
36 return ($cookie =~ /(?:^|\s)$cookie_name=([^;]*)/)[0];
39 sub create_auth_cookie
{
42 return "${cookie_name}=$ticket; path=/; secure;";
45 sub format_response_data
{
46 my($format, $res, $uri) = @_;
48 my $data = $res->{data
};
49 my $info = $res->{info
};
53 if ($format eq 'json') {
54 $ct = 'application/json';
55 $raw = to_json
($data, {utf8
=> 1, allow_nonref
=> 1});
56 } elsif ($format eq 'html') {
58 $raw = "<html><body>";
59 if (!is_success
($res->{status
})) {
60 my $msg = $res->{message
} || '';
61 $raw .= "<h1>ERROR $res->{status} $msg</h1>";
63 my $lnk = PVE
::JSONSchema
::method_get_child_link
($info);
64 if ($lnk && $data && $data->{data
} && is_success
($res->{status
})) {
66 my $href = $lnk->{href
};
67 if ($href =~ m/^\{(\S+)\}$/) {
69 $uri =~ s/\/+$//; # remove trailing slash
70 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data
}}) {
73 if (defined(my $value = $elem->{$prop})) {
75 if (scalar(keys %$elem) > 1) {
76 my $tv = to_json
($elem, {allow_nonref
=> 1, canonical
=> 1});
77 $raw .= "<a href='$uri/$value'>$value</a> <pre>$tv</pre><br>";
79 $raw .= "<a href='$uri/$value'>$value</a><br>";
87 $raw .= encode_entities
(to_json
($data, {utf8
=> 1, allow_nonref
=> 1, pretty
=> 1}));
90 $raw .= "</body></html>";
92 } elsif ($format eq 'png') {
95 # fixme: better to revove that whole png thing ?
100 if ($data && ref($data) && ref($data->{data
}) &&
101 $data->{data
}->{filename
}) {
102 $filename = $data->{data
}->{filename
};
103 $raw = PVE
::Tools
::file_get_contents
($filename);
106 } elsif ($format eq 'extjs') {
107 $ct = 'application/json';
108 $raw = to_json
($data, {utf8
=> 1, allow_nonref
=> 1});
109 } elsif ($format eq 'htmljs') {
110 # we use this for extjs file upload forms
112 $raw = encode_entities
(to_json
($data, {utf8
=> 1, allow_nonref
=> 1}));
115 $raw = to_json
($data, {utf8
=> 1, allow_nonref
=> 1, pretty
=> 1});
118 return wantarray ?
($raw, $ct) : $raw;
121 sub prepare_response_data
{
122 my ($format, $res) = @_;
126 data
=> $res->{data
},
128 if (scalar(keys %{$res->{errors
}})) {
130 $new->{errors
} = $res->{errors
};
133 if ($format eq 'extjs' || $format eq 'htmljs') {
134 # HACK: extjs wants 'success' property instead of useful HTTP status codes
135 if (is_error
($res->{status
})) {
137 $new->{message
} = $res->{message
} || status_message
($res->{status
});
138 $new->{status
} = $res->{status
} || HTTP_OK
;
139 $res->{message
} = undef;
140 $res->{status
} = HTTP_OK
;
142 $new->{success
} = $success;
145 if ($success && $res->{total
}) {
146 $new->{total
} = $res->{total
};
152 sub create_http_request
{
153 my ($uri, $method, $params) = @_;
155 # NOTE: HTTP::Request::Common::PUT is crap - so we use our own code
156 # borrowed from HTTP::Request::Common::POST
158 if ($method eq 'POST' || $method eq 'PUT') {
160 my $req = HTTP
::Request-
>new($method => $uri);
161 $req->header('Content-Type' => 'application/x-www-form-urlencoded');
163 # We use a temporary URI object to format
164 # the application/x-www-form-urlencoded content.
165 my $url = URI-
>new('http:');
166 $url->query_form(%$params);
167 my $content = $url->query;
168 if (defined($content)) {
169 $req->header('Content-Length' => length($content));
170 $req->content($content);
172 $req->header('Content-Length' => 0);
178 die "unknown method '$method'";
182 my($r, $clientip, $host, $method, $abs_uri, $ticket, $token, $params) = @_;
184 syslog
('info', "proxy start $method $host:$abs_uri");
186 my $ua = LWP
::UserAgent-
>new(
187 protocols_allowed
=> [ 'http', 'https' ],
191 $ua->default_header('cookie' => "${cookie_name}=$ticket") if $ticket;
192 $ua->default_header('CSRFPreventionToken' => $token) if $token;
193 $ua->default_header('PVEDisableProxy' => 'true');
194 $ua->default_header('PVEClientIP' => $clientip);
196 my $uri = URI-
>new();
198 if ($host eq 'localhost') {
199 $uri->scheme('http');
200 $uri->host('localhost');
203 $uri->scheme('https');
208 $uri->path($abs_uri);
211 if ($method eq 'GET') {
212 $uri->query_form($params);
213 $response = $ua->request(HTTP
::Request
::Common
::GET
($uri));
214 } elsif ($method eq 'POST' || $method eq 'PUT') {
215 $response = $ua->request(create_http_request
($uri, $method, $params));
216 } elsif ($method eq 'DELETE') {
217 $response = $ua->request(HTTP
::Request
::Common
::DELETE
($uri));
219 my $code = HTTP_NOT_IMPLEMENTED
;
220 $r->status_line("$code proxy method '$method' not implemented");
225 if (my $cookie = $response->header("Set-Cookie")) {
226 $r->err_headers_out()->add("Set-Cookie" => $cookie);
229 my $ct = $response->header('Content-Type');
231 my $code = $response->code;
234 if (my $message = $response->message) {
235 $r->status_line("$code $message");
238 $r->content_type($ct) if $ct;
239 my $raw = $response->decoded_content;
241 # note: do not use err_headers_out(), because mod_deflate has a bug,
242 # resulting in dup length (for exampe 'content-length: 89, 75')
243 $r->headers_out()->add('Content-Length' , length($raw));
246 syslog
('info', "proxy end $method $host:$abs_uri ($code)");
251 my $check_permissions = sub {
252 my ($rpcenv, $perm, $username, $param) = @_;
254 return 1 if !$username && $perm->{user
} eq 'world';
256 return 1 if $username eq 'root@pam';
258 die "permission check failed (user != root)\n" if !$perm;
260 return 1 if $perm->{user
} && $perm->{user
} eq 'all';
262 return 1 if $perm->{user
} && $perm->{user
} eq 'arg' &&
263 $username eq $param->{username
};
265 if ($perm->{path
} && $perm->{privs
}) {
266 my $path = PVE
::Tools
::template_replace
($perm->{path
}, $param);
267 if (!$rpcenv->check($username, $path, $perm->{privs
})) {
268 my $privstr = join(',', @{$perm->{privs
}});
269 die "Permission check failed ($path, $privstr)\n";
274 die "Permission check failed\n";
278 my ($clientip, $method, $abs_uri, $rel_uri, $ticket, $token, $params) = @_;
280 my $rpcenv = PVE
::RPCEnvironment
::get
();
282 eval { $rpcenv->init_request(); };
285 return { status
=> HTTP_INTERNAL_SERVER_ERROR
, message
=> $err };
290 my $require_auth = 1;
292 # explicitly allow some calls without auth
293 if (($rel_uri eq '/access/domains' && $method eq 'GET') ||
294 ($rel_uri eq '/access/ticket' && $method eq 'POST')) {
298 my ($username, $age);
303 die "No ticket\n" if !$ticket;
305 ($username, $age) = PVE
::AccessControl
::verify_ticket
($ticket);
307 PVE
::AccessControl
::verify_csrf_prevention_token
($username, $token)
308 if ($euid != 0) && ($method ne 'GET');
312 status
=> HTTP_UNAUTHORIZED
,
319 my ($handler, $info) = PVE
::API2-
>find_handler($method, $rel_uri, $uri_param);
320 if (!$handler || !$info) {
322 status
=> HTTP_NOT_IMPLEMENTED
,
323 message
=> "Method '$method $abs_uri' not implemented",
327 delete $params->{_dc
}; # remove disable cache parameter
329 foreach my $p (keys %{$params}) {
330 if (defined($uri_param->{$p})) {
332 status
=> HTTP_BAD_REQUEST
,
333 message
=> "Parameter verification failed - duplicate parameter '$p'",
336 $uri_param->{$p} = $params->{$p};
339 # check access permissions
340 eval { &$check_permissions($rpcenv, $info->{permissions
}, $username, $uri_param); };
343 status
=> HTTP_FORBIDDEN
,
348 if ($info->{proxyto
}) {
351 my $pn = $info->{proxyto
};
352 my $node = $uri_param->{$pn};
353 die "proxy parameter '$pn' does not exists" if !$node;
355 if ($node ne 'localhost' &&
356 $node ne PVE
::INotify
::nodename
()) {
357 $remip = PVE
::Cluster
::remote_node_ip
($node);
362 status
=> HTTP_INTERNAL_SERVER_ERROR
,
367 return { proxy
=> $remip };
371 # fixme: not sure if we should do that here, because we can't proxy those
372 # methods to other hosts?
373 return { proxy
=> 'localhost' } if $info->{protected
} && ($euid != 0);
375 # set environment variables
376 $rpcenv->set_language('C'); # fixme:
377 $rpcenv->set_user($username);
378 $rpcenv->set_client_ip($clientip);
379 $rpcenv->set_result_count(undef);
382 info
=> $info, # useful to format output
387 $resp->{data
} = $handler->handle($info, $uri_param);
389 if (my $count = $rpcenv->get_result_count()) {
390 $resp->{total
} = $count;
395 if (ref($err) eq "PVE::Exception") {
396 $resp->{status
} = $err->{code
} || HTTP_INTERNAL_SERVER_ERROR
;
397 $resp->{message
} = $err->{msg
} || $@;
398 $resp->{errors
} = $err->{errors
} if $err->{errors
};
400 $resp->{status
} = HTTP_INTERNAL_SERVER_ERROR
;
401 $resp->{message
} = $@;
405 $rpcenv->set_user(undef);
407 if ($rel_uri eq '/access/ticket') {
408 $resp->{ticket
} = $resp->{data
}->{ticket
};
411 # fixme: update ticket if too old
412 # $resp->{ticket} = update_ticket($ticket);
420 my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+(html
|json
|extjs
|png
|htmljs
)(\
/.*)?$/;
421 $rel_uri = '/' if !$rel_uri;
423 return wantarray ?
($rel_uri, $format) : $rel_uri;
426 my $known_methods = {
436 #syslog('info', "perl handler called");
438 my $method = $r->method;
439 my $clientip = $r->connection->remote_ip();
441 return HTTP_NOT_IMPLEMENTED
442 if !$known_methods->{$method};
444 my $cgi = CGI-
>new ($r);
446 my $params = $cgi->Vars();
448 my $cookie = $r->headers_in->{Cookie
};
449 my $token = $r->headers_in->{CSRFPreventionToken
};
451 my $ticket = extract_auth_cookie
($cookie);
455 my $abs_uri = $r->uri;
456 my ($rel_uri, $format) = split_abs_uri
($abs_uri);
457 return HTTP_NOT_IMPLEMENTED
if !$format;
459 my $res = rest_handler
($clientip, $method, $abs_uri, $rel_uri,
460 $ticket, $token, $params);
463 if (($res->{proxy
} ne 'localhost') && $r->headers_in->{'PVEDisableProxy'}) {
464 my $code = FORBIDDEN
;
466 $r->status_line("$code proxy loop detected - aborted ");
467 return $res->{status
};
469 return proxy_handler
($r, $clientip, $res->{proxy
}, $method,
470 $abs_uri, $ticket, $token, $params);
473 prepare_response_data
($format, $res);
475 if ($res->{ticket
}) {
476 my $cookie = create_auth_cookie
($res->{ticket
});
477 $r->err_headers_out()->add("Set-Cookie" => $cookie);
480 $r->status($res->{status
} || HTTP_OK
);
482 if ($res->{message
}) {
483 my ($firstline) = $res->{message
} =~ m/\A(.*)$/m;
484 $r->status_line("$res->{status} $firstline");
487 my ($raw, $ct) = format_response_data
($format, $res, $abs_uri);
488 $r->content_type ($ct);
490 # note: do not use err_headers_out(), because mod_deflate has a bug,
491 # resulting in dup length (for exampe 'content-length: 89, 75')
492 $r->headers_out()->add('Content-Length', length($raw));
495 #syslog('info', "perl handler end $res->{status}");