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