]>
Commit | Line | Data |
---|---|---|
aff192e6 DM |
1 | package PVE::REST; |
2 | ||
3 | use warnings; | |
4 | use strict; | |
5 | use Digest::SHA1 qw(sha1_base64); | |
6 | use PVE::Cluster; | |
7 | use PVE::SafeSyslog; | |
8 | use PVE::Tools; | |
9 | use PVE::API2; | |
10 | use Apache2::Const; | |
aff192e6 DM |
11 | use mod_perl2; |
12 | use JSON; | |
13 | use Digest::SHA; | |
14 | use LWP::UserAgent; | |
15 | use HTTP::Request::Common; | |
16 | use HTTP::Status qw(:constants :is status_message); | |
17 | use HTML::Entities; | |
18 | use PVE::JSONSchema; | |
19 | use PVE::AccessControl; | |
20 | use PVE::RPCEnvironment; | |
21 | ||
22 | use Data::Dumper; # fixme: remove | |
23 | ||
6c980a67 DM |
24 | # my $MaxRequestsPerChild = 200; |
25 | ||
aff192e6 DM |
26 | my $cookie_name = 'PVEAuthCookie'; |
27 | ||
28 | my $baseuri = "/api2"; | |
29 | ||
30 | # http://perl.apache.org/docs/2.0/api/Apache2/SubProcess.html | |
31 | ||
282184f5 DM |
32 | my $debug_enabled; |
33 | sub enable_debug { | |
34 | $debug_enabled = 1; | |
35 | } | |
36 | ||
37 | sub debug_msg { | |
38 | return if !$debug_enabled; | |
39 | syslog('info', @_); | |
40 | } | |
41 | ||
aff192e6 DM |
42 | sub extract_auth_cookie { |
43 | my ($cookie) = @_; | |
44 | ||
45 | return undef if !$cookie; | |
46 | ||
47 | return ($cookie =~ /(?:^|\s)$cookie_name=([^;]*)/)[0]; | |
48 | } | |
49 | ||
50 | sub create_auth_cookie { | |
51 | my ($ticket) = @_; | |
52 | ||
53 | return "${cookie_name}=$ticket; path=/; secure;"; | |
54 | } | |
55 | ||
56 | sub format_response_data { | |
57 | my($format, $res, $uri) = @_; | |
58 | ||
59 | my $data = $res->{data}; | |
60 | my $info = $res->{info}; | |
61 | ||
62 | my ($ct, $raw); | |
63 | ||
64 | if ($format eq 'json') { | |
65 | $ct = 'application/json'; | |
66 | $raw = to_json($data, {utf8 => 1, allow_nonref => 1}); | |
67 | } elsif ($format eq 'html') { | |
68 | $ct = 'text/html'; | |
69 | $raw = "<html><body>"; | |
70 | if (!is_success($res->{status})) { | |
71 | my $msg = $res->{message} || ''; | |
72 | $raw .= "<h1>ERROR $res->{status} $msg</h1>"; | |
73 | } | |
74 | my $lnk = PVE::JSONSchema::method_get_child_link($info); | |
75 | if ($lnk && $data && $data->{data} && is_success($res->{status})) { | |
76 | ||
77 | my $href = $lnk->{href}; | |
78 | if ($href =~ m/^\{(\S+)\}$/) { | |
79 | my $prop = $1; | |
80 | $uri =~ s/\/+$//; # remove trailing slash | |
81 | foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) { | |
82 | next if !ref($elem); | |
83 | ||
84 | if (defined(my $value = $elem->{$prop})) { | |
85 | if ($value ne '') { | |
86 | if (scalar(keys %$elem) > 1) { | |
87 | my $tv = to_json($elem, {allow_nonref => 1, canonical => 1}); | |
88 | $raw .= "<a href='$uri/$value'>$value</a> <pre>$tv</pre><br>"; | |
89 | } else { | |
90 | $raw .= "<a href='$uri/$value'>$value</a><br>"; | |
91 | } | |
92 | } | |
93 | } | |
94 | } | |
95 | } | |
96 | } else { | |
97 | $raw .= "<pre>"; | |
98 | $raw .= encode_entities(to_json($data, {utf8 => 1, allow_nonref => 1, pretty => 1})); | |
99 | $raw .= "</pre>"; | |
100 | } | |
101 | $raw .= "</body></html>"; | |
102 | ||
103 | } elsif ($format eq 'png') { | |
104 | $ct = 'image/png'; | |
105 | ||
106 | # fixme: better to revove that whole png thing ? | |
107 | ||
108 | my $filename; | |
109 | $raw = ''; | |
110 | ||
111 | if ($data && ref($data) && ref($data->{data}) && | |
112 | $data->{data}->{filename}) { | |
113 | $filename = $data->{data}->{filename}; | |
114 | $raw = PVE::Tools::file_get_contents($filename); | |
115 | } | |
116 | ||
117 | } elsif ($format eq 'extjs') { | |
118 | $ct = 'application/json'; | |
119 | $raw = to_json($data, {utf8 => 1, allow_nonref => 1}); | |
120 | } elsif ($format eq 'htmljs') { | |
121 | # we use this for extjs file upload forms | |
122 | $ct = 'text/html'; | |
123 | $raw = encode_entities(to_json($data, {utf8 => 1, allow_nonref => 1})); | |
124 | } else { | |
125 | $ct = 'text/plain'; | |
126 | $raw = to_json($data, {utf8 => 1, allow_nonref => 1, pretty => 1}); | |
127 | } | |
128 | ||
129 | return wantarray ? ($raw, $ct) : $raw; | |
130 | } | |
131 | ||
132 | sub prepare_response_data { | |
133 | my ($format, $res) = @_; | |
134 | ||
135 | my $success = 1; | |
136 | my $new = { | |
137 | data => $res->{data}, | |
138 | }; | |
139 | if (scalar(keys %{$res->{errors}})) { | |
140 | $success = 0; | |
141 | $new->{errors} = $res->{errors}; | |
142 | } | |
143 | ||
144 | if ($format eq 'extjs' || $format eq 'htmljs') { | |
145 | # HACK: extjs wants 'success' property instead of useful HTTP status codes | |
146 | if (is_error($res->{status})) { | |
147 | $success = 0; | |
148 | $new->{message} = $res->{message} || status_message($res->{status}); | |
149 | $new->{status} = $res->{status} || HTTP_OK; | |
150 | $res->{message} = undef; | |
151 | $res->{status} = HTTP_OK; | |
152 | } | |
153 | $new->{success} = $success; | |
154 | } | |
155 | ||
156 | if ($success && $res->{total}) { | |
157 | $new->{total} = $res->{total}; | |
158 | } | |
159 | ||
3dff4a9f DM |
160 | if ($success && $res->{changes}) { |
161 | $new->{changes} = $res->{changes}; | |
162 | } | |
163 | ||
aff192e6 DM |
164 | $res->{data} = $new; |
165 | } | |
166 | ||
167 | sub create_http_request { | |
168 | my ($uri, $method, $params) = @_; | |
169 | ||
170 | # NOTE: HTTP::Request::Common::PUT is crap - so we use our own code | |
171 | # borrowed from HTTP::Request::Common::POST | |
172 | ||
173 | if ($method eq 'POST' || $method eq 'PUT') { | |
174 | ||
175 | my $req = HTTP::Request->new($method => $uri); | |
176 | $req->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
177 | ||
178 | # We use a temporary URI object to format | |
179 | # the application/x-www-form-urlencoded content. | |
180 | my $url = URI->new('http:'); | |
181 | $url->query_form(%$params); | |
182 | my $content = $url->query; | |
183 | if (defined($content)) { | |
184 | $req->header('Content-Length' => length($content)); | |
185 | $req->content($content); | |
186 | } else { | |
187 | $req->header('Content-Length' => 0); | |
188 | } | |
189 | ||
190 | return $req; | |
191 | } | |
192 | ||
193 | die "unknown method '$method'"; | |
194 | } | |
195 | ||
196 | sub proxy_handler { | |
197 | my($r, $clientip, $host, $method, $abs_uri, $ticket, $token, $params) = @_; | |
198 | ||
282184f5 | 199 | debug_msg("proxy start $method $host:$abs_uri"); |
aff192e6 DM |
200 | |
201 | my $ua = LWP::UserAgent->new( | |
202 | protocols_allowed => [ 'http', 'https' ], | |
203 | timeout => 30, | |
204 | ); | |
205 | ||
206 | $ua->default_header('cookie' => "${cookie_name}=$ticket") if $ticket; | |
207 | $ua->default_header('CSRFPreventionToken' => $token) if $token; | |
208 | $ua->default_header('PVEDisableProxy' => 'true'); | |
209 | $ua->default_header('PVEClientIP' => $clientip); | |
210 | ||
211 | my $uri = URI->new(); | |
212 | ||
213 | if ($host eq 'localhost') { | |
214 | $uri->scheme('http'); | |
215 | $uri->host('localhost'); | |
216 | $uri->port(85); | |
217 | } else { | |
218 | $uri->scheme('https'); | |
219 | $uri->host($host); | |
220 | $uri->port(8006); | |
221 | } | |
222 | ||
223 | $uri->path($abs_uri); | |
224 | ||
225 | my $response; | |
226 | if ($method eq 'GET') { | |
227 | $uri->query_form($params); | |
228 | $response = $ua->request(HTTP::Request::Common::GET($uri)); | |
229 | } elsif ($method eq 'POST' || $method eq 'PUT') { | |
230 | $response = $ua->request(create_http_request($uri, $method, $params)); | |
231 | } elsif ($method eq 'DELETE') { | |
232 | $response = $ua->request(HTTP::Request::Common::DELETE($uri)); | |
233 | } else { | |
234 | my $code = HTTP_NOT_IMPLEMENTED; | |
235 | $r->status_line("$code proxy method '$method' not implemented"); | |
236 | return $code; | |
237 | } | |
238 | ||
239 | ||
240 | if (my $cookie = $response->header("Set-Cookie")) { | |
241 | $r->err_headers_out()->add("Set-Cookie" => $cookie); | |
242 | } | |
243 | ||
244 | my $ct = $response->header('Content-Type'); | |
245 | ||
246 | my $code = $response->code; | |
247 | $r->status($code); | |
248 | ||
249 | if (my $message = $response->message) { | |
250 | $r->status_line("$code $message"); | |
251 | } | |
252 | ||
253 | $r->content_type($ct) if $ct; | |
254 | my $raw = $response->decoded_content; | |
255 | ||
256 | # note: do not use err_headers_out(), because mod_deflate has a bug, | |
257 | # resulting in dup length (for exampe 'content-length: 89, 75') | |
258 | $r->headers_out()->add('Content-Length' , length($raw)); | |
259 | $r->print($raw); | |
260 | ||
282184f5 | 261 | debug_msg("proxy end $method $host:$abs_uri ($code)"); |
aff192e6 DM |
262 | |
263 | return OK; | |
264 | } | |
265 | ||
266 | my $check_permissions = sub { | |
267 | my ($rpcenv, $perm, $username, $param) = @_; | |
268 | ||
269 | return 1 if !$username && $perm->{user} eq 'world'; | |
270 | ||
271 | return 1 if $username eq 'root@pam'; | |
272 | ||
273 | die "permission check failed (user != root)\n" if !$perm; | |
274 | ||
275 | return 1 if $perm->{user} && $perm->{user} eq 'all'; | |
276 | ||
277 | return 1 if $perm->{user} && $perm->{user} eq 'arg' && | |
278 | $username eq $param->{username}; | |
279 | ||
280 | if ($perm->{path} && $perm->{privs}) { | |
281 | my $path = PVE::Tools::template_replace($perm->{path}, $param); | |
282 | if (!$rpcenv->check($username, $path, $perm->{privs})) { | |
283 | my $privstr = join(',', @{$perm->{privs}}); | |
284 | die "Permission check failed ($path, $privstr)\n"; | |
285 | } | |
286 | return 1; | |
287 | } | |
288 | ||
289 | die "Permission check failed\n"; | |
290 | }; | |
291 | ||
292 | sub rest_handler { | |
97ab5760 | 293 | my ($rpcenv, $clientip, $method, $abs_uri, $rel_uri, $ticket, $token) = @_; |
aff192e6 | 294 | |
97ab5760 DM |
295 | # set environment variables |
296 | $rpcenv->set_language('C'); # fixme: | |
297 | $rpcenv->set_client_ip($clientip); | |
aff192e6 | 298 | |
aff192e6 DM |
299 | my $euid = $>; |
300 | ||
301 | my $require_auth = 1; | |
302 | ||
303 | # explicitly allow some calls without auth | |
304 | if (($rel_uri eq '/access/domains' && $method eq 'GET') || | |
305 | ($rel_uri eq '/access/ticket' && $method eq 'POST')) { | |
306 | $require_auth = 0; | |
307 | } | |
308 | ||
309 | my ($username, $age); | |
310 | ||
97ab5760 DM |
311 | my $isUpload = 0; |
312 | ||
aff192e6 DM |
313 | if ($require_auth) { |
314 | ||
315 | eval { | |
316 | die "No ticket\n" if !$ticket; | |
317 | ||
318 | ($username, $age) = PVE::AccessControl::verify_ticket($ticket); | |
319 | ||
97ab5760 DM |
320 | $rpcenv->set_user($username); |
321 | ||
322 | if ($method eq 'POST' && $rel_uri =~ m|^/nodes/([^/]+)/storage/([^/]+)/upload$|) { | |
323 | my ($node, $storeid) = ($1, $2); | |
324 | my $perm = { | |
325 | path => "/storage/$storeid", | |
326 | privs => [ 'abc' ], | |
327 | }; | |
328 | &$check_permissions($rpcenv, $perm, $username, {}); | |
329 | $isUpload = 1; | |
330 | } | |
331 | ||
332 | # we skip CSRF check for file upload, because it is | |
333 | # difficult to pass CSRF HTTP headers with native html forms, | |
334 | # and it should not be necessary at all. | |
aff192e6 | 335 | PVE::AccessControl::verify_csrf_prevention_token($username, $token) |
97ab5760 | 336 | if !$isUpload && ($euid != 0) && ($method ne 'GET'); |
aff192e6 DM |
337 | }; |
338 | if (my $err = $@) { | |
339 | return { | |
340 | status => HTTP_UNAUTHORIZED, | |
341 | message => $err, | |
342 | }; | |
343 | } | |
344 | } | |
97ab5760 DM |
345 | |
346 | # we are authenticated now | |
347 | ||
aff192e6 DM |
348 | my $uri_param = {}; |
349 | my ($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param); | |
350 | if (!$handler || !$info) { | |
351 | return { | |
352 | status => HTTP_NOT_IMPLEMENTED, | |
353 | message => "Method '$method $abs_uri' not implemented", | |
354 | }; | |
355 | } | |
356 | ||
97ab5760 DM |
357 | # Note: we need to delay CGI parameter parsing until |
358 | # we are authenticated (avoid DOS (file upload) attacs) | |
359 | ||
360 | my $params; | |
361 | eval { $params = $rpcenv->parse_params($isUpload); }; | |
362 | if (my $err = $@) { | |
363 | return { | |
364 | status => HTTP_BAD_REQUEST, | |
365 | message => "parameter parser failed: $err", | |
366 | }; | |
367 | } | |
368 | ||
aff192e6 DM |
369 | delete $params->{_dc}; # remove disable cache parameter |
370 | ||
371 | foreach my $p (keys %{$params}) { | |
372 | if (defined($uri_param->{$p})) { | |
373 | return { | |
374 | status => HTTP_BAD_REQUEST, | |
375 | message => "Parameter verification failed - duplicate parameter '$p'", | |
376 | }; | |
377 | } | |
378 | $uri_param->{$p} = $params->{$p}; | |
379 | } | |
380 | ||
381 | # check access permissions | |
382 | eval { &$check_permissions($rpcenv, $info->{permissions}, $username, $uri_param); }; | |
383 | if (my $err = $@) { | |
384 | return { | |
385 | status => HTTP_FORBIDDEN, | |
386 | message => $err, | |
387 | }; | |
388 | } | |
389 | ||
390 | if ($info->{proxyto}) { | |
391 | my $remip; | |
392 | eval { | |
393 | my $pn = $info->{proxyto}; | |
394 | my $node = $uri_param->{$pn}; | |
395 | die "proxy parameter '$pn' does not exists" if !$node; | |
396 | ||
97ab5760 DM |
397 | if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) { |
398 | die "unable to proxy file uploads" if $isUpload; | |
aff192e6 DM |
399 | $remip = PVE::Cluster::remote_node_ip($node); |
400 | } | |
401 | }; | |
402 | if (my $err = $@) { | |
403 | return { | |
404 | status => HTTP_INTERNAL_SERVER_ERROR, | |
405 | message => $err, | |
406 | }; | |
407 | } | |
408 | if ($remip) { | |
97ab5760 | 409 | return { proxy => $remip, proxy_params => $params }; |
aff192e6 DM |
410 | } |
411 | } | |
412 | ||
97ab5760 DM |
413 | if ($info->{protected} && ($euid != 0)) { |
414 | if ($isUpload) { | |
415 | my $uinfo = $rpcenv->get_upload_info('filename'); | |
416 | $params->{tmpfilename} = $uinfo->{tmpfilename}; | |
417 | } | |
418 | return { proxy => 'localhost' , proxy_params => $params } | |
419 | } | |
aff192e6 DM |
420 | |
421 | my $resp = { | |
422 | info => $info, # useful to format output | |
423 | status => HTTP_OK, | |
424 | }; | |
425 | ||
426 | eval { | |
427 | $resp->{data} = $handler->handle($info, $uri_param); | |
428 | ||
429 | if (my $count = $rpcenv->get_result_count()) { | |
430 | $resp->{total} = $count; | |
431 | } | |
3dff4a9f DM |
432 | if (my $diff = $rpcenv->get_result_changes()) { |
433 | $resp->{changes} = $diff; | |
434 | } | |
aff192e6 DM |
435 | }; |
436 | my $err = $@; | |
437 | if ($err) { | |
438 | if (ref($err) eq "PVE::Exception") { | |
439 | $resp->{status} = $err->{code} || HTTP_INTERNAL_SERVER_ERROR; | |
440 | $resp->{message} = $err->{msg} || $@; | |
441 | $resp->{errors} = $err->{errors} if $err->{errors}; | |
442 | } else { | |
443 | $resp->{status} = HTTP_INTERNAL_SERVER_ERROR; | |
444 | $resp->{message} = $@; | |
445 | } | |
446 | } | |
447 | ||
448 | $rpcenv->set_user(undef); | |
449 | ||
450 | if ($rel_uri eq '/access/ticket') { | |
451 | $resp->{ticket} = $resp->{data}->{ticket}; | |
452 | } | |
453 | ||
454 | # fixme: update ticket if too old | |
455 | # $resp->{ticket} = update_ticket($ticket); | |
456 | ||
457 | return $resp; | |
458 | } | |
459 | ||
460 | sub split_abs_uri { | |
461 | my ($abs_uri) = @_; | |
462 | ||
463 | my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+(html|json|extjs|png|htmljs)(\/.*)?$/; | |
464 | $rel_uri = '/' if !$rel_uri; | |
465 | ||
466 | return wantarray ? ($rel_uri, $format) : $rel_uri; | |
467 | } | |
468 | ||
469 | my $known_methods = { | |
470 | GET => 1, | |
471 | POST => 1, | |
472 | PUT => 1, | |
473 | DELETE => 1, | |
474 | }; | |
475 | ||
6c980a67 DM |
476 | my $request_count = 0; |
477 | ||
aff192e6 DM |
478 | sub handler { |
479 | my($r) = @_; | |
480 | ||
282184f5 | 481 | debug_msg("perl handler called"); |
aff192e6 | 482 | |
6c980a67 DM |
483 | $request_count++; |
484 | # we do not use KeepAlive, so this is not necessary | |
485 | # $r->child_terminate() if $request_count >= $MaxRequestsPerChild; | |
486 | ||
aff192e6 DM |
487 | my $method = $r->method; |
488 | my $clientip = $r->connection->remote_ip(); | |
489 | ||
490 | return HTTP_NOT_IMPLEMENTED | |
491 | if !$known_methods->{$method}; | |
492 | ||
aff192e6 DM |
493 | my $cookie = $r->headers_in->{Cookie}; |
494 | my $token = $r->headers_in->{CSRFPreventionToken}; | |
495 | ||
496 | my $ticket = extract_auth_cookie($cookie); | |
497 | ||
498 | $r->no_cache (1); | |
499 | ||
500 | my $abs_uri = $r->uri; | |
501 | my ($rel_uri, $format) = split_abs_uri($abs_uri); | |
502 | return HTTP_NOT_IMPLEMENTED if !$format; | |
503 | ||
97ab5760 DM |
504 | my $rpcenv; |
505 | my $res; | |
506 | ||
507 | eval { | |
508 | $rpcenv = PVE::RPCEnvironment::get(); | |
509 | $rpcenv->init_request(request_rec => $r); | |
510 | }; | |
511 | if (my $err = $@) { | |
512 | syslog('err', $err); | |
513 | $res = { status => HTTP_INTERNAL_SERVER_ERROR, message => $err }; | |
514 | } else { | |
515 | $res = rest_handler($rpcenv, $clientip, $method, $abs_uri, $rel_uri, | |
516 | $ticket, $token); | |
517 | } | |
aff192e6 DM |
518 | |
519 | if ($res->{proxy}) { | |
520 | if (($res->{proxy} ne 'localhost') && $r->headers_in->{'PVEDisableProxy'}) { | |
521 | my $code = FORBIDDEN; | |
522 | $r->status($code); | |
523 | $r->status_line("$code proxy loop detected - aborted "); | |
524 | return $res->{status}; | |
525 | } | |
526 | return proxy_handler($r, $clientip, $res->{proxy}, $method, | |
97ab5760 | 527 | $abs_uri, $ticket, $token, $res->{proxy_params}); |
aff192e6 DM |
528 | } |
529 | ||
530 | prepare_response_data($format, $res); | |
531 | ||
532 | if ($res->{ticket}) { | |
533 | my $cookie = create_auth_cookie($res->{ticket}); | |
534 | $r->err_headers_out()->add("Set-Cookie" => $cookie); | |
535 | } | |
536 | ||
537 | $r->status($res->{status} || HTTP_OK); | |
538 | ||
539 | if ($res->{message}) { | |
540 | my ($firstline) = $res->{message} =~ m/\A(.*)$/m; | |
541 | $r->status_line("$res->{status} $firstline"); | |
542 | } | |
543 | ||
544 | my ($raw, $ct) = format_response_data($format, $res, $abs_uri); | |
545 | $r->content_type ($ct); | |
546 | ||
547 | # note: do not use err_headers_out(), because mod_deflate has a bug, | |
548 | # resulting in dup length (for exampe 'content-length: 89, 75') | |
549 | $r->headers_out()->add('Content-Length', length($raw)); | |
550 | $r->print($raw); | |
551 | ||
282184f5 | 552 | debug_msg("perl handler end $res->{status}"); |
aff192e6 DM |
553 | |
554 | return OK; | |
555 | } | |
556 | ||
557 | 1; |