]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/rest.rs
api2/admin/datastore/catar.rs: simplify/fix debug message
[proxmox-backup.git] / src / server / rest.rs
CommitLineData
d15009c0 1use crate::tools;
f17db0ab
DM
2use crate::api::schema::*;
3use crate::api::router::*;
4use crate::api::config::*;
b9903d63 5use crate::auth_helpers::*;
0f253593 6use super::environment::RestEnvironment;
1571873d 7use super::formatter::*;
16b48b81 8
826bb982 9use std::path::{Path, PathBuf};
9bc17e8d 10use std::sync::Arc;
e7ea17de 11use std::collections::HashMap;
9bc17e8d
DM
12
13use failure::*;
f4c514c1 14use serde_json::{json, Value};
e7ea17de 15use url::form_urlencoded;
9bc17e8d 16
9bc17e8d
DM
17use futures::future::{self, Either};
18//use tokio::prelude::*;
19//use tokio::timer::Delay;
20use tokio::fs::File;
9bc17e8d
DM
21//use bytes::{BytesMut, BufMut};
22
23//use hyper::body::Payload;
24use hyper::http::request::Parts;
10be1d29 25use hyper::{Body, Request, Response, StatusCode};
9bc17e8d
DM
26use hyper::service::{Service, NewService};
27use hyper::rt::{Future, Stream};
28use hyper::header;
29
4b2cdeb9
DM
30extern "C" { fn tzset(); }
31
f0b10921
DM
32pub struct RestServer {
33 pub api_config: Arc<ApiConfig>,
34}
35
36impl RestServer {
37
38 pub fn new(api_config: ApiConfig) -> Self {
39 Self { api_config: Arc::new(api_config) }
40 }
41}
42
43impl NewService for RestServer
44{
45 type ReqBody = Body;
46 type ResBody = Body;
47 type Error = hyper::Error;
48 type InitError = hyper::Error;
49 type Service = ApiService;
50 type Future = Box<Future<Item = Self::Service, Error = Self::InitError> + Send>;
51 fn new_service(&self) -> Self::Future {
52 Box::new(future::ok(ApiService { api_config: self.api_config.clone() }))
53 }
54}
55
56pub struct ApiService {
57 pub api_config: Arc<ApiConfig>,
58}
59
78a1fa67
DM
60impl ApiService {
61 fn log_response(path: &str, resp: &Response<Body>) {
7e03988c 62
7171b3e0 63 if resp.extensions().get::<NoLogExtension>().is_some() { return; };
7e03988c 64
78a1fa67 65 let status = resp.status();
7e03988c 66
78a1fa67
DM
67 if !status.is_success() {
68 let reason = status.canonical_reason().unwrap_or("unknown reason");
69 let client = "unknown"; // fixme: howto get peer_addr ?
44c00c0d
DM
70
71 let mut message = "request failed";
72 if let Some(data) = resp.extensions().get::<ErrorMessageExtension>() {
73 message = &data.0;
74 }
78a1fa67
DM
75
76 log::error!("{}: {} {}: [client {}] {}", path, status.as_str(), reason, client, message);
77 }
78 }
79}
f0b10921
DM
80
81impl Service for ApiService {
82 type ReqBody = Body;
83 type ResBody = Body;
84 type Error = hyper::Error;
85 type Future = Box<Future<Item = Response<Body>, Error = Self::Error> + Send>;
86
87 fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
78a1fa67
DM
88 let path = req.uri().path().to_owned();
89 Box::new(handle_request(self.api_config.clone(), req).then(move |result| {
f0b10921 90 match result {
78a1fa67
DM
91 Ok(res) => {
92 Self::log_response(&path, &res);
93 Ok::<_, hyper::Error>(res)
94 }
f0b10921 95 Err(err) => {
0dffe3f9 96 if let Some(apierr) = err.downcast_ref::<HttpError>() {
f0b10921
DM
97 let mut resp = Response::new(Body::from(apierr.message.clone()));
98 *resp.status_mut() = apierr.code;
78a1fa67 99 Self::log_response(&path, &resp);
f0b10921
DM
100 Ok(resp)
101 } else {
102 let mut resp = Response::new(Body::from(err.to_string()));
103 *resp.status_mut() = StatusCode::BAD_REQUEST;
78a1fa67 104 Self::log_response(&path, &resp);
f0b10921
DM
105 Ok(resp)
106 }
107 }
108 }
109 }))
110 }
111}
112
279ecfdf
DM
113fn get_request_parameters_async(
114 info: &'static ApiMethod,
9bc17e8d
DM
115 parts: Parts,
116 req_body: Body,
e7ea17de 117 uri_param: HashMap<String, String>,
279ecfdf 118) -> Box<Future<Item = Value, Error = failure::Error> + Send>
9bc17e8d
DM
119{
120 let resp = req_body
4dcf43d2 121 .map_err(|err| http_err!(BAD_REQUEST, format!("Promlems reading request body: {}", err)))
9bc17e8d
DM
122 .fold(Vec::new(), |mut acc, chunk| {
123 if acc.len() + chunk.len() < 64*1024 { //fimxe: max request body size?
124 acc.extend_from_slice(&*chunk);
125 Ok(acc)
126 }
4dcf43d2 127 else { Err(http_err!(BAD_REQUEST, format!("Request body too large"))) }
9bc17e8d
DM
128 })
129 .and_then(move |body| {
130
1ed86a0b 131 let utf8 = std::str::from_utf8(&body)?;
9bc17e8d 132
e7ea17de 133 let mut param_list: Vec<(String, String)> = vec![];
9bc17e8d 134
1ed86a0b
WB
135 if utf8.len() > 0 {
136 for (k, v) in form_urlencoded::parse(utf8.as_bytes()).into_owned() {
e7ea17de
DM
137 param_list.push((k, v));
138 }
139
9bc17e8d
DM
140 }
141
142 if let Some(query_str) = parts.uri.query() {
e7ea17de 143 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
1571873d 144 if k == "_dc" { continue; } // skip extjs "disable cache" parameter
e7ea17de 145 param_list.push((k, v));
9bc17e8d
DM
146 }
147 }
148
e7ea17de
DM
149 for (k, v) in uri_param {
150 param_list.push((k.clone(), v.clone()));
151 }
152
153 let params = parse_parameter_strings(&param_list, &info.parameters, true)?;
154
9bc17e8d
DM
155 Ok(params)
156 });
157
158 Box::new(resp)
159}
160
7171b3e0
DM
161struct NoLogExtension();
162
c8f3f9b1 163fn proxy_protected_request(
4b2cdeb9 164 info: &'static ApiMethod,
a3da38dd 165 mut parts: Parts,
f1204833 166 req_body: Body,
f1204833
DM
167) -> BoxFut
168{
169
a3da38dd
DM
170 let mut uri_parts = parts.uri.clone().into_parts();
171
172 uri_parts.scheme = Some(http::uri::Scheme::HTTP);
173 uri_parts.authority = Some(http::uri::Authority::from_static("127.0.0.1:82"));
174 let new_uri = http::Uri::from_parts(uri_parts).unwrap();
175
176 parts.uri = new_uri;
177
178 let request = Request::from_parts(parts, req_body);
179
180 let resp = hyper::client::Client::new()
181 .request(request)
7e03988c
DM
182 .map_err(|e| Error::from(e))
183 .map(|mut resp| {
7171b3e0 184 resp.extensions_mut().insert(NoLogExtension);
7e03988c
DM
185 resp
186 });
a3da38dd 187
4b2cdeb9
DM
188 let resp = if info.reload_timezone {
189 Either::A(resp.then(|resp| {unsafe { tzset() }; resp }))
190 } else {
191 Either::B(resp)
192 };
193
a3da38dd 194 return Box::new(resp);
f1204833
DM
195}
196
279ecfdf 197fn handle_sync_api_request(
e82dad97 198 mut rpcenv: RestEnvironment,
279ecfdf 199 info: &'static ApiMethod,
1571873d 200 formatter: &'static OutputFormatter,
9bc17e8d
DM
201 parts: Parts,
202 req_body: Body,
e7ea17de 203 uri_param: HashMap<String, String>,
279ecfdf 204) -> BoxFut
9bc17e8d 205{
e7ea17de 206 let params = get_request_parameters_async(info, parts, req_body, uri_param);
9bc17e8d 207
a154a8e8
DM
208 let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
209
9bc17e8d
DM
210 let resp = params
211 .and_then(move |params| {
a154a8e8 212 let mut delay = false;
6049b71f
DM
213 let resp = match (info.handler)(params, info, &mut rpcenv) {
214 Ok(data) => (formatter.format_result)(data, &rpcenv),
a154a8e8
DM
215 Err(err) => {
216 if let Some(httperr) = err.downcast_ref::<HttpError>() {
217 if httperr.code == StatusCode::UNAUTHORIZED { delay = true; }
218 }
219 (formatter.format_error)(err)
220 }
6049b71f 221 };
a154a8e8 222
4b2cdeb9
DM
223 if info.reload_timezone {
224 unsafe { tzset() };
225 }
226
a154a8e8
DM
227 if delay {
228 let delayed_response = tokio::timer::Delay::new(delay_unauth_time)
229 .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err)))
230 .and_then(|_| Ok(resp));
231
232 Either::A(delayed_response)
233 } else {
234 Either::B(future::ok(resp))
235 }
9bc17e8d
DM
236 });
237
238 Box::new(resp)
239}
240
50cfb695 241fn handle_async_api_request(
e82dad97 242 mut rpcenv: RestEnvironment,
50cfb695 243 info: &'static ApiAsyncMethod,
cf16af2a
DM
244 formatter: &'static OutputFormatter,
245 parts: Parts,
7e21da6e
DM
246 req_body: Body,
247 uri_param: HashMap<String, String>,
248) -> BoxFut
249{
250 // fixme: convert parameters to Json
251 let mut param_list: Vec<(String, String)> = vec![];
252
cf16af2a
DM
253 if let Some(query_str) = parts.uri.query() {
254 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
255 if k == "_dc" { continue; } // skip extjs "disable cache" parameter
256 param_list.push((k, v));
257 }
258 }
259
7e21da6e
DM
260 for (k, v) in uri_param {
261 param_list.push((k.clone(), v.clone()));
262 }
263
264 let params = match parse_parameter_strings(&param_list, &info.parameters, true) {
265 Ok(v) => v,
266 Err(err) => {
6049b71f 267 let resp = (formatter.format_error)(Error::from(err));
cf16af2a 268 return Box::new(future::ok(resp));
7e21da6e
DM
269 }
270 };
271
e82dad97 272 match (info.handler)(parts, req_body, params, info, &mut rpcenv) {
0ee0ad5b
DM
273 Ok(future) => future,
274 Err(err) => {
6049b71f 275 let resp = (formatter.format_error)(Error::from(err));
0ee0ad5b
DM
276 Box::new(future::ok(resp))
277 }
278 }
7e21da6e
DM
279}
280
f4c514c1
DM
281fn get_index() -> BoxFut {
282
d15009c0 283 let nodename = tools::nodename();
34f956bc
DM
284 let username = ""; // fixme: implement real auth
285 let token = "";
f4c514c1
DM
286
287 let setup = json!({
288 "Setup": { "auth_cookie_name": "PBSAuthCookie" },
289 "NodeName": nodename,
290 "UserName": username,
d15009c0 291 "CSRFPreventionToken": token,
f4c514c1
DM
292 });
293
294 let index = format!(r###"
295<!DOCTYPE html>
296<html>
297 <head>
298 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
299 <meta http-equiv="X-UA-Compatible" content="IE=edge">
300 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
301 <title>Proxmox Backup Server</title>
8adbdb0a 302 <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
f4c514c1 303 <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
8adbdb0a
DM
304 <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
305 <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
f4c514c1
DM
306 <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
307 <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
8adbdb0a
DM
308 <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
309 <script type="text/javascript" src="/extjs/charts-debug.js"></script>
f4c514c1
DM
310 <script type="text/javascript">
311 Proxmox = {};
312 </script>
8adbdb0a
DM
313 <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
314 <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
f4c514c1
DM
315 <script type="text/javascript">
316 Ext.History.fieldid = 'x-history-field';
317 </script>
5c7a1b15 318 <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
f4c514c1
DM
319 </head>
320 <body>
321 <!-- Fields required for history management -->
322 <form id="history-form" class="x-hidden">
323 <input type="hidden" id="x-history-field"/>
324 </form>
325 </body>
326</html>
327"###, setup.to_string());
328
d15009c0
DM
329 let resp = Response::builder()
330 .status(StatusCode::OK)
331 .header(header::CONTENT_TYPE, "text/html")
d15009c0
DM
332 .body(index.into())
333 .unwrap();
334
335 Box::new(future::ok(resp))
f4c514c1
DM
336}
337
826bb982
DM
338fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
339
340 if let Some(ext) = filename.extension().and_then(|osstr| osstr.to_str()) {
341 return match ext {
342 "css" => ("text/css", false),
343 "html" => ("text/html", false),
344 "js" => ("application/javascript", false),
345 "json" => ("application/json", false),
346 "map" => ("application/json", false),
347 "png" => ("image/png", true),
348 "ico" => ("image/x-icon", true),
349 "gif" => ("image/gif", true),
350 "svg" => ("image/svg+xml", false),
351 "jar" => ("application/java-archive", true),
352 "woff" => ("application/font-woff", true),
353 "woff2" => ("application/font-woff2", true),
354 "ttf" => ("application/font-snft", true),
355 "pdf" => ("application/pdf", true),
356 "epub" => ("application/epub+zip", true),
357 "mp3" => ("audio/mpeg", true),
358 "oga" => ("audio/ogg", true),
359 "tgz" => ("application/x-compressed-tar", true),
360 _ => ("application/octet-stream", false),
361 };
362 }
363
364 ("application/octet-stream", false)
365}
366
9bc17e8d
DM
367fn simple_static_file_download(filename: PathBuf) -> BoxFut {
368
826bb982
DM
369 let (content_type, _nocomp) = extension_to_content_type(&filename);
370
9bc17e8d 371 Box::new(File::open(filename)
4dcf43d2 372 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
826bb982 373 .and_then(move |file| {
9bc17e8d
DM
374 let buf: Vec<u8> = Vec::new();
375 tokio::io::read_to_end(file, buf)
4dcf43d2 376 .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err)))
826bb982
DM
377 .and_then(move |data| {
378 let mut response = Response::new(data.1.into());
379 response.headers_mut().insert(
380 header::CONTENT_TYPE,
381 header::HeaderValue::from_static(content_type));
382 Ok(response)
383 })
9bc17e8d
DM
384 }))
385}
386
387fn chuncked_static_file_download(filename: PathBuf) -> BoxFut {
388
826bb982
DM
389 let (content_type, _nocomp) = extension_to_content_type(&filename);
390
9bc17e8d 391 Box::new(File::open(filename)
4dcf43d2 392 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
826bb982 393 .and_then(move |file| {
059ca7c3 394 let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()).
9bc17e8d
DM
395 map(|bytes| {
396 //sigh - howto avoid copy here? or the whole map() ??
397 hyper::Chunk::from(bytes.to_vec())
398 });
399 let body = Body::wrap_stream(payload);
826bb982
DM
400
401 // fixme: set other headers ?
9bc17e8d
DM
402 Ok(Response::builder()
403 .status(StatusCode::OK)
826bb982 404 .header(header::CONTENT_TYPE, content_type)
9bc17e8d
DM
405 .body(body)
406 .unwrap())
407 }))
408}
409
410fn handle_static_file_download(filename: PathBuf) -> BoxFut {
411
412 let response = tokio::fs::metadata(filename.clone())
4dcf43d2 413 .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err)))
9bc17e8d
DM
414 .and_then(|metadata| {
415 if metadata.len() < 1024*32 {
416 Either::A(simple_static_file_download(filename))
417 } else {
418 Either::B(chuncked_static_file_download(filename))
419 }
420 });
421
422 return Box::new(response);
423}
424
425pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
426
427 let (parts, body) = req.into_parts();
428
429 let method = parts.method.clone();
430 let path = parts.uri.path();
431
432 // normalize path
433 // do not allow ".", "..", or hidden files ".XXXX"
434 // also remove empty path components
435
436 let items = path.split('/');
437 let mut path = String::new();
438 let mut components = vec![];
439
440 for name in items {
441 if name.is_empty() { continue; }
442 if name.starts_with(".") {
10be1d29 443 return Box::new(future::err(http_err!(BAD_REQUEST, "Path contains illegal components.".to_string())));
9bc17e8d
DM
444 }
445 path.push('/');
446 path.push_str(name);
447 components.push(name);
448 }
449
450 let comp_len = components.len();
451
452 println!("REQUEST {} {}", method, path);
453 println!("COMPO {:?}", components);
454
f1204833
DM
455 let env_type = api.env_type();
456 let mut rpcenv = RestEnvironment::new(env_type);
e82dad97 457
b9903d63
DM
458 let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
459
460 if let Some(raw_cookie) = parts.headers.get("COOKIE") {
461 if let Ok(cookie) = raw_cookie.to_str() {
462 if let Some(ticket) = tools::extract_auth_cookie(cookie, "PBSAuthCookie") {
463 if let Ok((_, Some(username))) = tools::ticket::verify_rsa_ticket(
464 public_auth_key(), "PBS", &ticket, None, -300, 3600*2) {
465 rpcenv.set_user(Some(username));
466 }
467 }
468 }
469 }
470
471
576e3bf2 472 if comp_len >= 1 && components[0] == "api2" {
9bc17e8d
DM
473 println!("GOT API REQUEST");
474 if comp_len >= 2 {
475 let format = components[1];
1571873d
DM
476 let formatter = match format {
477 "json" => &JSON_FORMATTER,
478 "extjs" => &EXTJS_FORMATTER,
479 _ => {
480 return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format))));
481 }
482 };
9bc17e8d 483
e7ea17de
DM
484 let mut uri_param = HashMap::new();
485
b9903d63
DM
486 if comp_len == 4 && components[2] == "access" && components[3] == "ticket" {
487 // explicitly allow those calls without auth
488 } else {
489 if let Some(_username) = rpcenv.get_user() {
490 // fixme: check permissions
491 } else {
492 // always delay unauthorized calls by 3 seconds (from start of request)
493 let resp = (formatter.format_error)(http_err!(UNAUTHORIZED, "permission check failed.".into()));
494 let delayed_response = tokio::timer::Delay::new(delay_unauth_time)
495 .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err)))
496 .and_then(|_| Ok(resp));
497
498 return Box::new(delayed_response);
499 }
500 }
d7d23785 501
7e21da6e
DM
502 match api.find_method(&components[2..], method, &mut uri_param) {
503 MethodDefinition::None => {}
504 MethodDefinition::Simple(api_method) => {
f1204833 505 if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
4b2cdeb9 506 return proxy_protected_request(api_method, parts, body);
f1204833
DM
507 } else {
508 return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
509 }
7e21da6e 510 }
50cfb695 511 MethodDefinition::Async(async_method) => {
e82dad97 512 return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param);
7e21da6e 513 }
9bc17e8d
DM
514 }
515 }
516 } else {
517 // not Auth for accessing files!
518
f4c514c1
DM
519 if comp_len == 0 {
520 return get_index();
521 } else {
522 let filename = api.find_alias(&components);
523 return handle_static_file_download(filename);
524 }
9bc17e8d
DM
525 }
526
10be1d29 527 Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string())))
9bc17e8d 528}