]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/rest.rs
src/server/rest.rs: improve logs for unauthorized request
[proxmox-backup.git] / src / server / rest.rs
CommitLineData
d15009c0 1use crate::tools;
ef2f2efb 2use crate::api_schema::*;
dc9a007b
DM
3use crate::api_schema::router::*;
4use crate::api_schema::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
d4736445 60fn log_response(method: hyper::Method, path: &str, resp: &Response<Body>) {
7e03988c 61
d4736445 62 if resp.extensions().get::<NoLogExtension>().is_some() { return; };
7e03988c 63
d4736445 64 let status = resp.status();
7e03988c 65
d4736445
DM
66 if !status.is_success() {
67 let reason = status.canonical_reason().unwrap_or("unknown reason");
68 let client = "unknown"; // fixme: howto get peer_addr ?
44c00c0d 69
d4736445
DM
70 let mut message = "request failed";
71 if let Some(data) = resp.extensions().get::<ErrorMessageExtension>() {
72 message = &data.0;
78a1fa67 73 }
d4736445
DM
74
75 log::error!("{} {}: {} {}: [client {}] {}", method.as_str(), path, status.as_str(), reason, client, message);
78a1fa67
DM
76 }
77}
f0b10921
DM
78
79impl 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 {
78a1fa67 86 let path = req.uri().path().to_owned();
d4736445
DM
87 let method = req.method().clone();
88
78a1fa67 89 Box::new(handle_request(self.api_config.clone(), req).then(move |result| {
f0b10921 90 match result {
78a1fa67 91 Ok(res) => {
d4736445 92 log_response(method, &path, &res);
78a1fa67
DM
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;
d4736445 99 log_response(method, &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;
d4736445 104 log_response(method, &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
5ddf8cb1
DM
425fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>) {
426
427 let mut ticket = None;
428 if let Some(raw_cookie) = headers.get("COOKIE") {
429 if let Ok(cookie) = raw_cookie.to_str() {
430 ticket = tools::extract_auth_cookie(cookie, "PBSAuthCookie");
431 }
432 }
433
434 let token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) {
435 Some(Ok(v)) => Some(v.to_owned()),
436 _ => None,
437 };
438
439 (ticket, token)
440}
441
442fn check_auth(method: &hyper::Method, ticket: Option<String>, token: Option<String>) -> Result<String, Error> {
443
444 let ticket_lifetime = 3600*2; // 2 hours
445
446 let username = match ticket {
447 Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) {
448 Ok((_age, Some(username))) => username.to_owned(),
449 Ok((_, None)) => bail!("ticket without username."),
450 Err(err) => return Err(err),
451 }
452 None => bail!("missing ticket"),
453 };
454
455 if method != hyper::Method::GET {
456 if let Some(token) = token {
8225aa2f 457 println!("CSRF prevention token: {:?}", token);
5ddf8cb1
DM
458 verify_csrf_prevention_token(csrf_secret(), &username, &token, -300, ticket_lifetime)?;
459 } else {
8225aa2f 460 bail!("missing CSRF prevention token");
5ddf8cb1
DM
461 }
462 }
463
464 Ok(username)
465}
466
9bc17e8d
DM
467pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
468
469 let (parts, body) = req.into_parts();
470
471 let method = parts.method.clone();
472 let path = parts.uri.path();
473
474 // normalize path
475 // do not allow ".", "..", or hidden files ".XXXX"
476 // also remove empty path components
477
478 let items = path.split('/');
479 let mut path = String::new();
480 let mut components = vec![];
481
482 for name in items {
483 if name.is_empty() { continue; }
484 if name.starts_with(".") {
10be1d29 485 return Box::new(future::err(http_err!(BAD_REQUEST, "Path contains illegal components.".to_string())));
9bc17e8d
DM
486 }
487 path.push('/');
488 path.push_str(name);
489 components.push(name);
490 }
491
492 let comp_len = components.len();
493
494 println!("REQUEST {} {}", method, path);
495 println!("COMPO {:?}", components);
496
f1204833
DM
497 let env_type = api.env_type();
498 let mut rpcenv = RestEnvironment::new(env_type);
e82dad97 499
b9903d63
DM
500 let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
501
576e3bf2 502 if comp_len >= 1 && components[0] == "api2" {
9bc17e8d 503 println!("GOT API REQUEST");
5ddf8cb1 504
9bc17e8d
DM
505 if comp_len >= 2 {
506 let format = components[1];
1571873d
DM
507 let formatter = match format {
508 "json" => &JSON_FORMATTER,
509 "extjs" => &EXTJS_FORMATTER,
510 _ => {
511 return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format))));
512 }
513 };
9bc17e8d 514
e7ea17de
DM
515 let mut uri_param = HashMap::new();
516
b9903d63
DM
517 if comp_len == 4 && components[2] == "access" && components[3] == "ticket" {
518 // explicitly allow those calls without auth
519 } else {
5ddf8cb1
DM
520 let (ticket, token) = extract_auth_data(&parts.headers);
521 match check_auth(&method, ticket, token) {
522 Ok(username) => {
523
524 // fixme: check permissions
525
526 rpcenv.set_user(Some(username));
527 }
528 Err(err) => {
529 // always delay unauthorized calls by 3 seconds (from start of request)
530 let err = http_err!(UNAUTHORIZED, format!("permission check failed - {}", err));
531 let resp = (formatter.format_error)(err);
532 let delayed_response = tokio::timer::Delay::new(delay_unauth_time)
533 .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err)))
534 .and_then(|_| Ok(resp));
535
536 return Box::new(delayed_response);
537 }
b9903d63
DM
538 }
539 }
d7d23785 540
7e21da6e
DM
541 match api.find_method(&components[2..], method, &mut uri_param) {
542 MethodDefinition::None => {}
543 MethodDefinition::Simple(api_method) => {
f1204833 544 if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
4b2cdeb9 545 return proxy_protected_request(api_method, parts, body);
f1204833
DM
546 } else {
547 return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
548 }
7e21da6e 549 }
50cfb695 550 MethodDefinition::Async(async_method) => {
e82dad97 551 return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param);
7e21da6e 552 }
9bc17e8d
DM
553 }
554 }
555 } else {
556 // not Auth for accessing files!
557
f4c514c1
DM
558 if comp_len == 0 {
559 return get_index();
560 } else {
561 let filename = api.find_alias(&components);
562 return handle_static_file_download(filename);
563 }
9bc17e8d
DM
564 }
565
10be1d29 566 Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string())))
9bc17e8d 567}