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