]> git.proxmox.com Git - pve-manager.git/blob - PVE/REST.pm
a0249651d4b09aff2d3f729536ee44db71c44e2a
[pve-manager.git] / PVE / REST.pm
1 package PVE::REST;
2
3 use warnings;
4 use strict;
5 use English;
6 use PVE::Cluster;
7 use PVE::SafeSyslog;
8 use PVE::Tools;
9 use PVE::API2;
10 use JSON;
11 use LWP::UserAgent;
12 use HTTP::Request::Common;
13 use HTTP::Status qw(:constants :is status_message);
14 use HTML::Entities;
15 use PVE::Exception qw(raise raise_perm_exc);
16 use PVE::JSONSchema;
17 use PVE::AccessControl;
18 use PVE::RPCEnvironment;
19 use URI::Escape;
20
21 use Data::Dumper; # fixme: remove
22
23 my $cookie_name = 'PVEAuthCookie';
24
25 sub extract_auth_cookie {
26 my ($cookie) = @_;
27
28 return undef if !$cookie;
29
30 my $ticket = ($cookie =~ /(?:^|\s)$cookie_name=([^;]*)/)[0];
31
32 if ($ticket && $ticket =~ m/^PVE%3A/) {
33 $ticket = uri_unescape($ticket);
34 }
35
36 return $ticket;
37 }
38
39 sub create_auth_cookie {
40 my ($ticket) = @_;
41
42 my $encticket = uri_escape($ticket);
43 return "${cookie_name}=$encticket; path=/; secure;";
44 }
45
46 sub format_response_data {
47 my($format, $res, $uri) = @_;
48
49 my $data = $res->{data};
50 my $info = $res->{info};
51
52 my ($ct, $raw, $nocomp);
53
54 if ($format eq 'json') {
55 $ct = 'application/json;charset=UTF-8';
56 $raw = to_json($data, {utf8 => 1, allow_nonref => 1});
57 } elsif ($format eq 'html') {
58 $ct = 'text/html;charset=UTF-8';
59 $raw = "<html><body>";
60 if (!is_success($res->{status})) {
61 my $msg = $res->{message} || '';
62 $raw .= "<h1>ERROR $res->{status} $msg</h1>";
63 }
64 my $lnk = PVE::JSONSchema::method_get_child_link($info);
65 if ($lnk && $data && $data->{data} && is_success($res->{status})) {
66
67 my $href = $lnk->{href};
68 if ($href =~ m/^\{(\S+)\}$/) {
69 my $prop = $1;
70 $uri =~ s/\/+$//; # remove trailing slash
71 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) {
72 next if !ref($elem);
73
74 if (defined(my $value = $elem->{$prop})) {
75 if ($value ne '') {
76 if (scalar(keys %$elem) > 1) {
77 my $tv = to_json($elem, {allow_nonref => 1, canonical => 1});
78 $raw .= "<a href='$uri/$value'>$value</a> <pre>$tv</pre><br>";
79 } else {
80 $raw .= "<a href='$uri/$value'>$value</a><br>";
81 }
82 }
83 }
84 }
85 }
86 } else {
87 $raw .= "<pre>";
88 $raw .= encode_entities(to_json($data, {allow_nonref => 1, pretty => 1}));
89 $raw .= "</pre>";
90 }
91 $raw .= "</body></html>";
92
93 } elsif ($format eq 'png') {
94 $ct = 'image/png';
95 $nocomp = 1;
96 # fixme: better to revove that whole png thing ?
97
98 my $filename;
99 $raw = '';
100
101 if ($data && ref($data) && ref($data->{data}) &&
102 $data->{data}->{filename} && defined($data->{data}->{image})) {
103 $filename = $data->{data}->{filename};
104 $raw = $data->{data}->{image};
105 }
106
107 } elsif ($format eq 'extjs') {
108 $ct = 'application/json;charset=UTF-8';
109 $raw = to_json($data, {utf8 => 1, allow_nonref => 1});
110 } elsif ($format eq 'htmljs') {
111 # we use this for extjs file upload forms
112 $ct = 'text/html;charset=UTF-8';
113 $raw = encode_entities(to_json($data, {allow_nonref => 1}));
114 } elsif ($format eq 'spiceconfig') {
115 $ct = 'application/x-virt-viewer;charset=UTF-8';
116 if ($data && ref($data) && ref($data->{data})) {
117 $raw = "[virt-viewer]\n";
118 while (my ($key, $value) = each %{$data->{data}}) {
119 $raw .= "$key=$value\n" if defined($value);
120 }
121 }
122 } else {
123 $ct = 'text/plain;charset=UTF-8';
124 $raw = to_json($data, {utf8 => 1, allow_nonref => 1, pretty => 1});
125 }
126
127 return wantarray ? ($raw, $ct, $nocomp) : $raw;
128 }
129
130 sub prepare_response_data {
131 my ($format, $res) = @_;
132
133 my $success = 1;
134 my $new = {
135 data => $res->{data},
136 };
137 if (scalar(keys %{$res->{errors}})) {
138 $success = 0;
139 $new->{errors} = $res->{errors};
140 }
141
142 if ($format eq 'extjs' || $format eq 'htmljs') {
143 # HACK: extjs wants 'success' property instead of useful HTTP status codes
144 if (is_error($res->{status})) {
145 $success = 0;
146 $new->{message} = $res->{message} || status_message($res->{status});
147 $new->{status} = $res->{status} || 200;
148 $res->{message} = undef;
149 $res->{status} = 200;
150 }
151 $new->{success} = $success;
152 }
153
154 if ($success && $res->{total}) {
155 $new->{total} = $res->{total};
156 }
157
158 if ($success && $res->{changes}) {
159 $new->{changes} = $res->{changes};
160 }
161
162 $res->{data} = $new;
163 }
164
165 my $exc_to_res = sub {
166 my ($err, $status) = @_;
167
168 $status = $status || HTTP_INTERNAL_SERVER_ERROR;
169
170 my $resp = {};
171 if (ref($err) eq "PVE::Exception") {
172 $resp->{status} = $err->{code} || $status;
173 $resp->{errors} = $err->{errors} if $err->{errors};
174 $resp->{message} = $err->{msg};
175 } else {
176 $resp->{status} = $status;
177 $resp->{message} = $err;
178 }
179
180 return $resp;
181 };
182
183 sub auth_handler {
184 my ($rpcenv, $clientip, $method, $rel_uri, $ticket, $token) = @_;
185
186 # set environment variables
187 $rpcenv->set_user(undef);
188 $rpcenv->set_language('C'); # fixme:
189 $rpcenv->set_client_ip($clientip);
190
191 my $require_auth = 1;
192
193 # explicitly allow some calls without auth
194 if (($rel_uri eq '/access/domains' && $method eq 'GET') ||
195 ($rel_uri eq '/access/ticket' && $method eq 'POST')) {
196 $require_auth = 0;
197 }
198
199 my ($username, $age);
200
201 my $isUpload = 0;
202
203 if ($require_auth) {
204
205 die "No ticket\n" if !$ticket;
206
207 ($username, $age) = PVE::AccessControl::verify_ticket($ticket);
208
209 $rpcenv->set_user($username);
210
211 if ($method eq 'POST' && $rel_uri =~ m|^/nodes/([^/]+)/storage/([^/]+)/upload$|) {
212 my ($node, $storeid) = ($1, $2);
213 # we disable CSRF checks if $isUpload is set,
214 # to improve security we check user upload permission here
215 my $perm = { check => ['perm', "/storage/$storeid", ['Datastore.AllocateTemplate']] };
216 $rpcenv->check_api2_permissions($perm, $username, {});
217 $isUpload = 1;
218 }
219
220 # we skip CSRF check for file upload, because it is
221 # difficult to pass CSRF HTTP headers with native html forms,
222 # and it should not be necessary at all.
223 PVE::AccessControl::verify_csrf_prevention_token($username, $token)
224 if !$isUpload && ($EUID != 0) && ($method ne 'GET');
225 }
226
227 return {
228 ticket => $ticket,
229 token => $token,
230 userid => $username,
231 age => $age,
232 isUpload => $isUpload,
233 };
234 }
235
236 sub rest_handler {
237 my ($rpcenv, $clientip, $method, $rel_uri, $auth, $params) = @_;
238
239 my $uri_param = {};
240 my ($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param);
241 if (!$handler || !$info) {
242 return {
243 status => HTTP_NOT_IMPLEMENTED,
244 message => "Method '$method $rel_uri' not implemented",
245 };
246 }
247
248 foreach my $p (keys %{$params}) {
249 if (defined($uri_param->{$p})) {
250 return {
251 status => HTTP_BAD_REQUEST,
252 message => "Parameter verification failed - duplicate parameter '$p'",
253 };
254 }
255 $uri_param->{$p} = $params->{$p};
256 }
257
258 # check access permissions
259 eval { $rpcenv->check_api2_permissions($info->{permissions}, $auth->{userid}, $uri_param); };
260 if (my $err = $@) {
261 return &$exc_to_res($err, HTTP_FORBIDDEN);
262 }
263
264 if ($info->{proxyto}) {
265 my $remip;
266 eval {
267 my $pn = $info->{proxyto};
268 my $node = $uri_param->{$pn};
269 die "proxy parameter '$pn' does not exists" if !$node;
270
271 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
272 die "unable to proxy file uploads" if $auth->{isUpload};
273 $remip = PVE::Cluster::remote_node_ip($node);
274 }
275 };
276 if (my $err = $@) {
277 return &$exc_to_res($err);
278 }
279 if ($remip) {
280 return { proxy => $remip, proxy_params => $params };
281 }
282 }
283
284 if ($info->{protected} && ($EUID != 0)) {
285 return { proxy => 'localhost' , proxy_params => $params }
286 }
287
288 my $resp = {
289 info => $info, # useful to format output
290 status => HTTP_OK,
291 };
292
293 eval {
294 $resp->{data} = $handler->handle($info, $uri_param);
295
296 if (my $count = $rpcenv->get_result_attrib('total')) {
297 $resp->{total} = $count;
298 }
299 if (my $diff = $rpcenv->get_result_attrib('changes')) {
300 $resp->{changes} = $diff;
301 }
302 };
303 if (my $err = $@) {
304 return &$exc_to_res($err);
305 }
306
307 return $resp;
308 }
309
310 1;