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