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