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