]> git.proxmox.com Git - proxmox-backup.git/blob - src/server/rest.rs
move normalize_path to tools::normalize_uri_path
[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<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() {
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<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<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 return Ok(params);
159 }
160
161 let mut param_list: Vec<(String, String)> = vec![];
162
163 if utf8.len() > 0 {
164 for (k, v) in form_urlencoded::parse(utf8.as_bytes()).into_owned() {
165 param_list.push((k, v));
166 }
167
168 }
169
170 if let Some(query_str) = parts.uri.query() {
171 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
172 if k == "_dc" { continue; } // skip extjs "disable cache" parameter
173 param_list.push((k, v));
174 }
175 }
176
177 for (k, v) in uri_param {
178 param_list.push((k.clone(), v.clone()));
179 }
180
181 let params = parse_parameter_strings(&param_list, obj_schema, true)?;
182
183 Ok(params)
184 });
185
186 Box::new(resp)
187 }
188
189 struct NoLogExtension();
190
191 fn proxy_protected_request(
192 info: &'static ApiMethod,
193 mut parts: Parts,
194 req_body: Body,
195 ) -> BoxFut
196 {
197
198 let mut uri_parts = parts.uri.clone().into_parts();
199
200 uri_parts.scheme = Some(http::uri::Scheme::HTTP);
201 uri_parts.authority = Some(http::uri::Authority::from_static("127.0.0.1:82"));
202 let new_uri = http::Uri::from_parts(uri_parts).unwrap();
203
204 parts.uri = new_uri;
205
206 let request = Request::from_parts(parts, req_body);
207
208 let resp = hyper::client::Client::new()
209 .request(request)
210 .map_err(Error::from)
211 .map(|mut resp| {
212 resp.extensions_mut().insert(NoLogExtension());
213 resp
214 });
215
216
217 let resp = if info.reload_timezone {
218 Either::A(resp.then(|resp| {unsafe { tzset() }; resp }))
219 } else {
220 Either::B(resp)
221 };
222
223 return Box::new(resp);
224 }
225
226 fn handle_sync_api_request(
227 mut rpcenv: RestEnvironment,
228 info: &'static ApiMethod,
229 formatter: &'static OutputFormatter,
230 parts: Parts,
231 req_body: Body,
232 uri_param: HashMap<String, String>,
233 ) -> BoxFut
234 {
235 let params = get_request_parameters_async(info, parts, req_body, uri_param);
236
237 let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
238
239 let resp = params
240 .and_then(move |params| {
241 let mut delay = false;
242 let resp = match (info.handler.as_ref().unwrap())(params, info, &mut rpcenv) {
243 Ok(data) => (formatter.format_result)(data, &rpcenv),
244 Err(err) => {
245 if let Some(httperr) = err.downcast_ref::<HttpError>() {
246 if httperr.code == StatusCode::UNAUTHORIZED { delay = true; }
247 }
248 (formatter.format_error)(err)
249 }
250 };
251
252 if info.reload_timezone {
253 unsafe { tzset() };
254 }
255
256 if delay {
257 Either::A(delayed_response(resp, delay_unauth_time))
258 } else {
259 Either::B(future::ok(resp))
260 }
261 });
262
263 Box::new(resp)
264 }
265
266 fn handle_async_api_request(
267 mut rpcenv: RestEnvironment,
268 info: &'static ApiAsyncMethod,
269 formatter: &'static OutputFormatter,
270 parts: Parts,
271 req_body: Body,
272 uri_param: HashMap<String, String>,
273 ) -> BoxFut
274 {
275 // fixme: convert parameters to Json
276 let mut param_list: Vec<(String, String)> = vec![];
277
278 if let Some(query_str) = parts.uri.query() {
279 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
280 if k == "_dc" { continue; } // skip extjs "disable cache" parameter
281 param_list.push((k, v));
282 }
283 }
284
285 for (k, v) in uri_param {
286 param_list.push((k.clone(), v.clone()));
287 }
288
289 let params = match parse_parameter_strings(&param_list, &info.parameters, true) {
290 Ok(v) => v,
291 Err(err) => {
292 let resp = (formatter.format_error)(Error::from(err));
293 return Box::new(future::ok(resp));
294 }
295 };
296
297 match (info.handler)(parts, req_body, params, info, &mut rpcenv) {
298 Ok(future) => future,
299 Err(err) => {
300 let resp = (formatter.format_error)(Error::from(err));
301 Box::new(future::ok(resp))
302 }
303 }
304 }
305
306 fn get_index(username: Option<String>, token: Option<String>) -> Response<Body> {
307
308 let nodename = tools::nodename();
309 let username = username.unwrap_or(String::from(""));
310
311 let token = token.unwrap_or(String::from(""));
312
313 let setup = json!({
314 "Setup": { "auth_cookie_name": "PBSAuthCookie" },
315 "NodeName": nodename,
316 "UserName": username,
317 "CSRFPreventionToken": token,
318 });
319
320 let index = format!(r###"
321 <!DOCTYPE html>
322 <html>
323 <head>
324 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
325 <meta http-equiv="X-UA-Compatible" content="IE=edge">
326 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
327 <title>Proxmox Backup Server</title>
328 <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
329 <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
330 <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
331 <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
332 <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
333 <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
334 <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
335 <script type="text/javascript" src="/extjs/charts-debug.js"></script>
336 <script type="text/javascript">
337 Proxmox = {};
338 </script>
339 <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
340 <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
341 <script type="text/javascript">
342 Ext.History.fieldid = 'x-history-field';
343 </script>
344 <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
345 </head>
346 <body>
347 <!-- Fields required for history management -->
348 <form id="history-form" class="x-hidden">
349 <input type="hidden" id="x-history-field"/>
350 </form>
351 </body>
352 </html>
353 "###, setup.to_string());
354
355 Response::builder()
356 .status(StatusCode::OK)
357 .header(header::CONTENT_TYPE, "text/html")
358 .body(index.into())
359 .unwrap()
360 }
361
362 fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
363
364 if let Some(ext) = filename.extension().and_then(|osstr| osstr.to_str()) {
365 return match ext {
366 "css" => ("text/css", false),
367 "html" => ("text/html", false),
368 "js" => ("application/javascript", false),
369 "json" => ("application/json", false),
370 "map" => ("application/json", false),
371 "png" => ("image/png", true),
372 "ico" => ("image/x-icon", true),
373 "gif" => ("image/gif", true),
374 "svg" => ("image/svg+xml", false),
375 "jar" => ("application/java-archive", true),
376 "woff" => ("application/font-woff", true),
377 "woff2" => ("application/font-woff2", true),
378 "ttf" => ("application/font-snft", true),
379 "pdf" => ("application/pdf", true),
380 "epub" => ("application/epub+zip", true),
381 "mp3" => ("audio/mpeg", true),
382 "oga" => ("audio/ogg", true),
383 "tgz" => ("application/x-compressed-tar", true),
384 _ => ("application/octet-stream", false),
385 };
386 }
387
388 ("application/octet-stream", false)
389 }
390
391 fn simple_static_file_download(filename: PathBuf) -> BoxFut {
392
393 let (content_type, _nocomp) = extension_to_content_type(&filename);
394
395 Box::new(File::open(filename)
396 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
397 .and_then(move |file| {
398 let buf: Vec<u8> = Vec::new();
399 tokio::io::read_to_end(file, buf)
400 .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err)))
401 .and_then(move |data| {
402 let mut response = Response::new(data.1.into());
403 response.headers_mut().insert(
404 header::CONTENT_TYPE,
405 header::HeaderValue::from_static(content_type));
406 Ok(response)
407 })
408 }))
409 }
410
411 fn chuncked_static_file_download(filename: PathBuf) -> BoxFut {
412
413 let (content_type, _nocomp) = extension_to_content_type(&filename);
414
415 Box::new(File::open(filename)
416 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
417 .and_then(move |file| {
418 let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()).
419 map(|bytes| {
420 //sigh - howto avoid copy here? or the whole map() ??
421 hyper::Chunk::from(bytes.to_vec())
422 });
423 let body = Body::wrap_stream(payload);
424
425 // fixme: set other headers ?
426 Ok(Response::builder()
427 .status(StatusCode::OK)
428 .header(header::CONTENT_TYPE, content_type)
429 .body(body)
430 .unwrap())
431 }))
432 }
433
434 fn handle_static_file_download(filename: PathBuf) -> BoxFut {
435
436 let response = tokio::fs::metadata(filename.clone())
437 .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err)))
438 .and_then(|metadata| {
439 if metadata.len() < 1024*32 {
440 Either::A(simple_static_file_download(filename))
441 } else {
442 Either::B(chuncked_static_file_download(filename))
443 }
444 });
445
446 return Box::new(response);
447 }
448
449 fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>) {
450
451 let mut ticket = None;
452 if let Some(raw_cookie) = headers.get("COOKIE") {
453 if let Ok(cookie) = raw_cookie.to_str() {
454 ticket = tools::extract_auth_cookie(cookie, "PBSAuthCookie");
455 }
456 }
457
458 let token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) {
459 Some(Ok(v)) => Some(v.to_owned()),
460 _ => None,
461 };
462
463 (ticket, token)
464 }
465
466 fn check_auth(method: &hyper::Method, ticket: &Option<String>, token: &Option<String>) -> Result<String, Error> {
467
468 let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
469
470 let username = match ticket {
471 Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) {
472 Ok((_age, Some(username))) => username.to_owned(),
473 Ok((_, None)) => bail!("ticket without username."),
474 Err(err) => return Err(err),
475 }
476 None => bail!("missing ticket"),
477 };
478
479 if method != hyper::Method::GET {
480 if let Some(token) = token {
481 println!("CSRF prevention token: {:?}", token);
482 verify_csrf_prevention_token(csrf_secret(), &username, &token, -300, ticket_lifetime)?;
483 } else {
484 bail!("missing CSRF prevention token");
485 }
486 }
487
488 Ok(username)
489 }
490
491 fn delayed_response(resp: Response<Body>, delay_unauth_time: std::time::Instant) -> BoxFut {
492
493 Box::new(tokio::timer::Delay::new(delay_unauth_time)
494 .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err)))
495 .and_then(|_| Ok(resp)))
496 }
497
498 pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
499
500 let (parts, body) = req.into_parts();
501
502 let method = parts.method.clone();
503
504 let (path, components) = match tools::normalize_uri_path(parts.uri.path()) {
505 Ok((p,c)) => (p, c),
506 Err(err) => return Box::new(future::err(http_err!(BAD_REQUEST, err.to_string()))),
507 };
508
509 let comp_len = components.len();
510
511 println!("REQUEST {} {}", method, path);
512 println!("COMPO {:?}", components);
513
514 let env_type = api.env_type();
515 let mut rpcenv = RestEnvironment::new(env_type);
516
517 let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
518
519 if comp_len >= 1 && components[0] == "api2" {
520
521 if comp_len >= 2 {
522 let format = components[1];
523 let formatter = match format {
524 "json" => &JSON_FORMATTER,
525 "extjs" => &EXTJS_FORMATTER,
526 _ => {
527 return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format))));
528 }
529 };
530
531 let mut uri_param = HashMap::new();
532
533 if comp_len == 4 && components[2] == "access" && components[3] == "ticket" {
534 // explicitly allow those calls without auth
535 } else {
536 let (ticket, token) = extract_auth_data(&parts.headers);
537 match check_auth(&method, &ticket, &token) {
538 Ok(username) => {
539
540 // fixme: check permissions
541
542 rpcenv.set_user(Some(username));
543 }
544 Err(err) => {
545 // always delay unauthorized calls by 3 seconds (from start of request)
546 let err = http_err!(UNAUTHORIZED, format!("permission check failed - {}", err));
547 return delayed_response((formatter.format_error)(err), delay_unauth_time);
548 }
549 }
550 }
551
552 match api.find_method(&components[2..], method, &mut uri_param) {
553 MethodDefinition::None => {
554 let err = http_err!(NOT_FOUND, "Path not found.".to_string());
555 return Box::new(future::ok((formatter.format_error)(err)));
556 }
557 MethodDefinition::Simple(api_method) => {
558 if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
559 return proxy_protected_request(api_method, parts, body);
560 } else {
561 return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
562 }
563 }
564 MethodDefinition::Async(async_method) => {
565 return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param);
566 }
567 }
568 }
569 } else {
570 // not Auth required for accessing files!
571
572 if method != hyper::Method::GET {
573 return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported method"))));
574 }
575
576 if comp_len == 0 {
577 let (ticket, token) = extract_auth_data(&parts.headers);
578 if ticket != None {
579 match check_auth(&method, &ticket, &token) {
580 Ok(username) => {
581 let new_token = assemble_csrf_prevention_token(csrf_secret(), &username);
582 return Box::new(future::ok(get_index(Some(username), Some(new_token))));
583 }
584 _ => return delayed_response(get_index(None, None), delay_unauth_time),
585 }
586 } else {
587 return Box::new(future::ok(get_index(None, None)));
588 }
589 } else {
590 let filename = api.find_alias(&components);
591 return handle_static_file_download(filename);
592 }
593 }
594
595 Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string())))
596 }