]>
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; | |
11 | use CGI; | |
12 | use mod_perl2; | |
13 | use JSON; | |
14 | use Digest::SHA; | |
15 | use LWP::UserAgent; | |
16 | use HTTP::Request::Common; | |
17 | use HTTP::Status qw(:constants :is status_message); | |
18 | use HTML::Entities; | |
19 | use PVE::JSONSchema; | |
20 | use PVE::AccessControl; | |
21 | use PVE::RPCEnvironment; | |
22 | ||
23 | use Data::Dumper; # fixme: remove | |
24 | ||
25 | my $cookie_name = 'PVEAuthCookie'; | |
26 | ||
27 | my $baseuri = "/api2"; | |
28 | ||
29 | # http://perl.apache.org/docs/2.0/api/Apache2/SubProcess.html | |
30 | ||
31 | sub extract_auth_cookie { | |
32 | my ($cookie) = @_; | |
33 | ||
34 | return undef if !$cookie; | |
35 | ||
36 | return ($cookie =~ /(?:^|\s)$cookie_name=([^;]*)/)[0]; | |
37 | } | |
38 | ||
39 | sub create_auth_cookie { | |
40 | my ($ticket) = @_; | |
41 | ||
42 | return "${cookie_name}=$ticket; path=/; secure;"; | |
43 | } | |
44 | ||
45 | sub format_response_data { | |
46 | my($format, $res, $uri) = @_; | |
47 | ||
48 | my $data = $res->{data}; | |
49 | my $info = $res->{info}; | |
50 | ||
51 | my ($ct, $raw); | |
52 | ||
53 | if ($format eq 'json') { | |
54 | $ct = 'application/json'; | |
55 | $raw = to_json($data, {utf8 => 1, allow_nonref => 1}); | |
56 | } elsif ($format eq 'html') { | |
57 | $ct = 'text/html'; | |
58 | $raw = "<html><body>"; | |
59 | if (!is_success($res->{status})) { | |
60 | my $msg = $res->{message} || ''; | |
61 | $raw .= "<h1>ERROR $res->{status} $msg</h1>"; | |
62 | } | |
63 | my $lnk = PVE::JSONSchema::method_get_child_link($info); | |
64 | if ($lnk && $data && $data->{data} && is_success($res->{status})) { | |
65 | ||
66 | my $href = $lnk->{href}; | |
67 | if ($href =~ m/^\{(\S+)\}$/) { | |
68 | my $prop = $1; | |
69 | $uri =~ s/\/+$//; # remove trailing slash | |
70 | foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) { | |
71 | next if !ref($elem); | |
72 | ||
73 | if (defined(my $value = $elem->{$prop})) { | |
74 | if ($value ne '') { | |
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>"; | |
78 | } else { | |
79 | $raw .= "<a href='$uri/$value'>$value</a><br>"; | |
80 | } | |
81 | } | |
82 | } | |
83 | } | |
84 | } | |
85 | } else { | |
86 | $raw .= "<pre>"; | |
87 | $raw .= encode_entities(to_json($data, {utf8 => 1, allow_nonref => 1, pretty => 1})); | |
88 | $raw .= "</pre>"; | |
89 | } | |
90 | $raw .= "</body></html>"; | |
91 | ||
92 | } elsif ($format eq 'png') { | |
93 | $ct = 'image/png'; | |
94 | ||
95 | # fixme: better to revove that whole png thing ? | |
96 | ||
97 | my $filename; | |
98 | $raw = ''; | |
99 | ||
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); | |
104 | } | |
105 | ||
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 | |
111 | $ct = 'text/html'; | |
112 | $raw = encode_entities(to_json($data, {utf8 => 1, allow_nonref => 1})); | |
113 | } else { | |
114 | $ct = 'text/plain'; | |
115 | $raw = to_json($data, {utf8 => 1, allow_nonref => 1, pretty => 1}); | |
116 | } | |
117 | ||
118 | return wantarray ? ($raw, $ct) : $raw; | |
119 | } | |
120 | ||
121 | sub prepare_response_data { | |
122 | my ($format, $res) = @_; | |
123 | ||
124 | my $success = 1; | |
125 | my $new = { | |
126 | data => $res->{data}, | |
127 | }; | |
128 | if (scalar(keys %{$res->{errors}})) { | |
129 | $success = 0; | |
130 | $new->{errors} = $res->{errors}; | |
131 | } | |
132 | ||
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})) { | |
136 | $success = 0; | |
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; | |
141 | } | |
142 | $new->{success} = $success; | |
143 | } | |
144 | ||
145 | if ($success && $res->{total}) { | |
146 | $new->{total} = $res->{total}; | |
147 | } | |
148 | ||
149 | $res->{data} = $new; | |
150 | } | |
151 | ||
152 | sub create_http_request { | |
153 | my ($uri, $method, $params) = @_; | |
154 | ||
155 | # NOTE: HTTP::Request::Common::PUT is crap - so we use our own code | |
156 | # borrowed from HTTP::Request::Common::POST | |
157 | ||
158 | if ($method eq 'POST' || $method eq 'PUT') { | |
159 | ||
160 | my $req = HTTP::Request->new($method => $uri); | |
161 | $req->header('Content-Type' => 'application/x-www-form-urlencoded'); | |
162 | ||
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); | |
171 | } else { | |
172 | $req->header('Content-Length' => 0); | |
173 | } | |
174 | ||
175 | return $req; | |
176 | } | |
177 | ||
178 | die "unknown method '$method'"; | |
179 | } | |
180 | ||
181 | sub proxy_handler { | |
182 | my($r, $clientip, $host, $method, $abs_uri, $ticket, $token, $params) = @_; | |
183 | ||
184 | syslog('info', "proxy start $method $host:$abs_uri"); | |
185 | ||
186 | my $ua = LWP::UserAgent->new( | |
187 | protocols_allowed => [ 'http', 'https' ], | |
188 | timeout => 30, | |
189 | ); | |
190 | ||
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); | |
195 | ||
196 | my $uri = URI->new(); | |
197 | ||
198 | if ($host eq 'localhost') { | |
199 | $uri->scheme('http'); | |
200 | $uri->host('localhost'); | |
201 | $uri->port(85); | |
202 | } else { | |
203 | $uri->scheme('https'); | |
204 | $uri->host($host); | |
205 | $uri->port(8006); | |
206 | } | |
207 | ||
208 | $uri->path($abs_uri); | |
209 | ||
210 | my $response; | |
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)); | |
218 | } else { | |
219 | my $code = HTTP_NOT_IMPLEMENTED; | |
220 | $r->status_line("$code proxy method '$method' not implemented"); | |
221 | return $code; | |
222 | } | |
223 | ||
224 | ||
225 | if (my $cookie = $response->header("Set-Cookie")) { | |
226 | $r->err_headers_out()->add("Set-Cookie" => $cookie); | |
227 | } | |
228 | ||
229 | my $ct = $response->header('Content-Type'); | |
230 | ||
231 | my $code = $response->code; | |
232 | $r->status($code); | |
233 | ||
234 | if (my $message = $response->message) { | |
235 | $r->status_line("$code $message"); | |
236 | } | |
237 | ||
238 | $r->content_type($ct) if $ct; | |
239 | my $raw = $response->decoded_content; | |
240 | ||
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)); | |
244 | $r->print($raw); | |
245 | ||
246 | syslog('info', "proxy end $method $host:$abs_uri ($code)"); | |
247 | ||
248 | return OK; | |
249 | } | |
250 | ||
251 | my $check_permissions = sub { | |
252 | my ($rpcenv, $perm, $username, $param) = @_; | |
253 | ||
254 | return 1 if !$username && $perm->{user} eq 'world'; | |
255 | ||
256 | return 1 if $username eq 'root@pam'; | |
257 | ||
258 | die "permission check failed (user != root)\n" if !$perm; | |
259 | ||
260 | return 1 if $perm->{user} && $perm->{user} eq 'all'; | |
261 | ||
262 | return 1 if $perm->{user} && $perm->{user} eq 'arg' && | |
263 | $username eq $param->{username}; | |
264 | ||
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"; | |
270 | } | |
271 | return 1; | |
272 | } | |
273 | ||
274 | die "Permission check failed\n"; | |
275 | }; | |
276 | ||
277 | sub rest_handler { | |
278 | my ($clientip, $method, $abs_uri, $rel_uri, $ticket, $token, $params) = @_; | |
279 | ||
280 | my $rpcenv = PVE::RPCEnvironment::get(); | |
281 | ||
282 | eval { $rpcenv->init_request(); }; | |
283 | if (my $err = $@) { | |
284 | syslog('err', $err); | |
285 | return { status => HTTP_INTERNAL_SERVER_ERROR, message => $err }; | |
286 | } | |
287 | ||
288 | my $euid = $>; | |
289 | ||
290 | my $require_auth = 1; | |
291 | ||
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')) { | |
295 | $require_auth = 0; | |
296 | } | |
297 | ||
298 | my ($username, $age); | |
299 | ||
300 | if ($require_auth) { | |
301 | ||
302 | eval { | |
303 | die "No ticket\n" if !$ticket; | |
304 | ||
305 | ($username, $age) = PVE::AccessControl::verify_ticket($ticket); | |
306 | ||
307 | PVE::AccessControl::verify_csrf_prevention_token($username, $token) | |
308 | if ($euid != 0) && ($method ne 'GET'); | |
309 | }; | |
310 | if (my $err = $@) { | |
311 | return { | |
312 | status => HTTP_UNAUTHORIZED, | |
313 | message => $err, | |
314 | }; | |
315 | } | |
316 | } | |
317 | ||
318 | my $uri_param = {}; | |
319 | my ($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param); | |
320 | if (!$handler || !$info) { | |
321 | return { | |
322 | status => HTTP_NOT_IMPLEMENTED, | |
323 | message => "Method '$method $abs_uri' not implemented", | |
324 | }; | |
325 | } | |
326 | ||
327 | delete $params->{_dc}; # remove disable cache parameter | |
328 | ||
329 | foreach my $p (keys %{$params}) { | |
330 | if (defined($uri_param->{$p})) { | |
331 | return { | |
332 | status => HTTP_BAD_REQUEST, | |
333 | message => "Parameter verification failed - duplicate parameter '$p'", | |
334 | }; | |
335 | } | |
336 | $uri_param->{$p} = $params->{$p}; | |
337 | } | |
338 | ||
339 | # check access permissions | |
340 | eval { &$check_permissions($rpcenv, $info->{permissions}, $username, $uri_param); }; | |
341 | if (my $err = $@) { | |
342 | return { | |
343 | status => HTTP_FORBIDDEN, | |
344 | message => $err, | |
345 | }; | |
346 | } | |
347 | ||
348 | if ($info->{proxyto}) { | |
349 | my $remip; | |
350 | eval { | |
351 | my $pn = $info->{proxyto}; | |
352 | my $node = $uri_param->{$pn}; | |
353 | die "proxy parameter '$pn' does not exists" if !$node; | |
354 | ||
355 | if ($node ne 'localhost' && | |
356 | $node ne PVE::INotify::nodename()) { | |
357 | $remip = PVE::Cluster::remote_node_ip($node); | |
358 | } | |
359 | }; | |
360 | if (my $err = $@) { | |
361 | return { | |
362 | status => HTTP_INTERNAL_SERVER_ERROR, | |
363 | message => $err, | |
364 | }; | |
365 | } | |
366 | if ($remip) { | |
367 | return { proxy => $remip }; | |
368 | } | |
369 | } | |
370 | ||
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); | |
374 | ||
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); | |
380 | ||
381 | my $resp = { | |
382 | info => $info, # useful to format output | |
383 | status => HTTP_OK, | |
384 | }; | |
385 | ||
386 | eval { | |
387 | $resp->{data} = $handler->handle($info, $uri_param); | |
388 | ||
389 | if (my $count = $rpcenv->get_result_count()) { | |
390 | $resp->{total} = $count; | |
391 | } | |
392 | }; | |
393 | my $err = $@; | |
394 | if ($err) { | |
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}; | |
399 | } else { | |
400 | $resp->{status} = HTTP_INTERNAL_SERVER_ERROR; | |
401 | $resp->{message} = $@; | |
402 | } | |
403 | } | |
404 | ||
405 | $rpcenv->set_user(undef); | |
406 | ||
407 | if ($rel_uri eq '/access/ticket') { | |
408 | $resp->{ticket} = $resp->{data}->{ticket}; | |
409 | } | |
410 | ||
411 | # fixme: update ticket if too old | |
412 | # $resp->{ticket} = update_ticket($ticket); | |
413 | ||
414 | return $resp; | |
415 | } | |
416 | ||
417 | sub split_abs_uri { | |
418 | my ($abs_uri) = @_; | |
419 | ||
420 | my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+(html|json|extjs|png|htmljs)(\/.*)?$/; | |
421 | $rel_uri = '/' if !$rel_uri; | |
422 | ||
423 | return wantarray ? ($rel_uri, $format) : $rel_uri; | |
424 | } | |
425 | ||
426 | my $known_methods = { | |
427 | GET => 1, | |
428 | POST => 1, | |
429 | PUT => 1, | |
430 | DELETE => 1, | |
431 | }; | |
432 | ||
433 | sub handler { | |
434 | my($r) = @_; | |
435 | ||
436 | #syslog('info', "perl handler called"); | |
437 | ||
438 | my $method = $r->method; | |
439 | my $clientip = $r->connection->remote_ip(); | |
440 | ||
441 | return HTTP_NOT_IMPLEMENTED | |
442 | if !$known_methods->{$method}; | |
443 | ||
444 | my $cgi = CGI->new ($r); | |
445 | ||
446 | my $params = $cgi->Vars(); | |
447 | ||
448 | my $cookie = $r->headers_in->{Cookie}; | |
449 | my $token = $r->headers_in->{CSRFPreventionToken}; | |
450 | ||
451 | my $ticket = extract_auth_cookie($cookie); | |
452 | ||
453 | $r->no_cache (1); | |
454 | ||
455 | my $abs_uri = $r->uri; | |
456 | my ($rel_uri, $format) = split_abs_uri($abs_uri); | |
457 | return HTTP_NOT_IMPLEMENTED if !$format; | |
458 | ||
459 | my $res = rest_handler($clientip, $method, $abs_uri, $rel_uri, | |
460 | $ticket, $token, $params); | |
461 | ||
462 | if ($res->{proxy}) { | |
463 | if (($res->{proxy} ne 'localhost') && $r->headers_in->{'PVEDisableProxy'}) { | |
464 | my $code = FORBIDDEN; | |
465 | $r->status($code); | |
466 | $r->status_line("$code proxy loop detected - aborted "); | |
467 | return $res->{status}; | |
468 | } | |
469 | return proxy_handler($r, $clientip, $res->{proxy}, $method, | |
470 | $abs_uri, $ticket, $token, $params); | |
471 | } | |
472 | ||
473 | prepare_response_data($format, $res); | |
474 | ||
475 | if ($res->{ticket}) { | |
476 | my $cookie = create_auth_cookie($res->{ticket}); | |
477 | $r->err_headers_out()->add("Set-Cookie" => $cookie); | |
478 | } | |
479 | ||
480 | $r->status($res->{status} || HTTP_OK); | |
481 | ||
482 | if ($res->{message}) { | |
483 | my ($firstline) = $res->{message} =~ m/\A(.*)$/m; | |
484 | $r->status_line("$res->{status} $firstline"); | |
485 | } | |
486 | ||
487 | my ($raw, $ct) = format_response_data($format, $res, $abs_uri); | |
488 | $r->content_type ($ct); | |
489 | ||
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)); | |
493 | $r->print($raw); | |
494 | ||
495 | #syslog('info', "perl handler end $res->{status}"); | |
496 | ||
497 | return OK; | |
498 | } | |
499 | ||
500 | 1; |