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