]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/rest.rs
server/rest.rs: add proxy_sync_api_request() dummy
[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::*;
0f253593 5use super::environment::RestEnvironment;
1571873d 6use super::formatter::*;
16b48b81 7
4dcf43d2 8use std::fmt;
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
f0b10921
DM
30pub struct RestServer {
31 pub api_config: Arc<ApiConfig>,
32}
33
34impl RestServer {
35
36 pub fn new(api_config: ApiConfig) -> Self {
37 Self { api_config: Arc::new(api_config) }
38 }
39}
40
41impl NewService for RestServer
42{
43 type ReqBody = Body;
44 type ResBody = Body;
45 type Error = hyper::Error;
46 type InitError = hyper::Error;
47 type Service = ApiService;
48 type Future = Box<Future<Item = Self::Service, Error = Self::InitError> + Send>;
49 fn new_service(&self) -> Self::Future {
50 Box::new(future::ok(ApiService { api_config: self.api_config.clone() }))
51 }
52}
53
54pub struct ApiService {
55 pub api_config: Arc<ApiConfig>,
56}
57
58
59impl Service for ApiService {
60 type ReqBody = Body;
61 type ResBody = Body;
62 type Error = hyper::Error;
63 type Future = Box<Future<Item = Response<Body>, Error = Self::Error> + Send>;
64
65 fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
66
67 Box::new(handle_request(self.api_config.clone(), req).then(|result| {
68 match result {
69 Ok(res) => Ok::<_, hyper::Error>(res),
70 Err(err) => {
1571873d 71 if let Some(apierr) = err.downcast_ref::<HttpError>() {
f0b10921
DM
72 let mut resp = Response::new(Body::from(apierr.message.clone()));
73 *resp.status_mut() = apierr.code;
74 Ok(resp)
75 } else {
76 let mut resp = Response::new(Body::from(err.to_string()));
77 *resp.status_mut() = StatusCode::BAD_REQUEST;
78 Ok(resp)
79 }
80 }
81 }
82 }))
83 }
84}
85
4dcf43d2
DM
86#[derive(Debug, Fail)]
87pub struct HttpError {
88 pub code: StatusCode,
89 pub message: String,
90}
91
92impl HttpError {
93 pub fn new(code: StatusCode, message: String) -> Self {
94 HttpError { code, message }
95 }
96}
97
98impl fmt::Display for HttpError {
99 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100 write!(f, "Error {}: {}", self.code, self.message)
101 }
102}
103
104macro_rules! http_err {
105 ($status:ident, $msg:expr) => {{
106 Error::from(HttpError::new(StatusCode::$status, $msg))
107 }}
108}
109
279ecfdf
DM
110fn get_request_parameters_async(
111 info: &'static ApiMethod,
9bc17e8d
DM
112 parts: Parts,
113 req_body: Body,
e7ea17de 114 uri_param: HashMap<String, String>,
279ecfdf 115) -> Box<Future<Item = Value, Error = failure::Error> + Send>
9bc17e8d
DM
116{
117 let resp = req_body
4dcf43d2 118 .map_err(|err| http_err!(BAD_REQUEST, format!("Promlems reading request body: {}", err)))
9bc17e8d
DM
119 .fold(Vec::new(), |mut acc, chunk| {
120 if acc.len() + chunk.len() < 64*1024 { //fimxe: max request body size?
121 acc.extend_from_slice(&*chunk);
122 Ok(acc)
123 }
4dcf43d2 124 else { Err(http_err!(BAD_REQUEST, format!("Request body too large"))) }
9bc17e8d
DM
125 })
126 .and_then(move |body| {
127
1ed86a0b 128 let utf8 = std::str::from_utf8(&body)?;
9bc17e8d 129
1ed86a0b 130 println!("GOT BODY {:?}", utf8);
9bc17e8d 131
e7ea17de 132 let mut param_list: Vec<(String, String)> = vec![];
9bc17e8d 133
1ed86a0b
WB
134 if utf8.len() > 0 {
135 for (k, v) in form_urlencoded::parse(utf8.as_bytes()).into_owned() {
e7ea17de
DM
136 param_list.push((k, v));
137 }
138
9bc17e8d
DM
139 }
140
141 if let Some(query_str) = parts.uri.query() {
e7ea17de 142 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
1571873d 143 if k == "_dc" { continue; } // skip extjs "disable cache" parameter
e7ea17de 144 param_list.push((k, v));
9bc17e8d
DM
145 }
146 }
147
e7ea17de
DM
148 for (k, v) in uri_param {
149 param_list.push((k.clone(), v.clone()));
150 }
151
152 let params = parse_parameter_strings(&param_list, &info.parameters, true)?;
153
9bc17e8d
DM
154 println!("GOT PARAMS {}", params);
155 Ok(params)
156 });
157
158 Box::new(resp)
159}
160
f1204833
DM
161fn proxy_sync_api_request(
162 mut rpcenv: RestEnvironment,
163 info: &'static ApiMethod,
164 formatter: &'static OutputFormatter,
165 parts: Parts,
166 req_body: Body,
167 uri_param: HashMap<String, String>,
168) -> BoxFut
169{
170
171 return Box::new(future::err(http_err!(BAD_REQUEST, String::from("implement proxy"))));
172}
173
279ecfdf 174fn handle_sync_api_request(
e82dad97 175 mut rpcenv: RestEnvironment,
279ecfdf 176 info: &'static ApiMethod,
1571873d 177 formatter: &'static OutputFormatter,
9bc17e8d
DM
178 parts: Parts,
179 req_body: Body,
e7ea17de 180 uri_param: HashMap<String, String>,
279ecfdf 181) -> BoxFut
9bc17e8d 182{
e7ea17de 183 let params = get_request_parameters_async(info, parts, req_body, uri_param);
9bc17e8d
DM
184
185 let resp = params
186 .and_then(move |params| {
6049b71f
DM
187 let resp = match (info.handler)(params, info, &mut rpcenv) {
188 Ok(data) => (formatter.format_result)(data, &rpcenv),
189 Err(err) => (formatter.format_error)(err),
190 };
191 Ok(resp)
9bc17e8d
DM
192 });
193
194 Box::new(resp)
195}
196
50cfb695 197fn handle_async_api_request(
e82dad97 198 mut rpcenv: RestEnvironment,
50cfb695 199 info: &'static ApiAsyncMethod,
cf16af2a
DM
200 formatter: &'static OutputFormatter,
201 parts: Parts,
7e21da6e
DM
202 req_body: Body,
203 uri_param: HashMap<String, String>,
204) -> BoxFut
205{
206 // fixme: convert parameters to Json
207 let mut param_list: Vec<(String, String)> = vec![];
208
cf16af2a
DM
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
7e21da6e
DM
216 for (k, v) in uri_param {
217 param_list.push((k.clone(), v.clone()));
218 }
219
220 let params = match parse_parameter_strings(&param_list, &info.parameters, true) {
221 Ok(v) => v,
222 Err(err) => {
6049b71f 223 let resp = (formatter.format_error)(Error::from(err));
cf16af2a 224 return Box::new(future::ok(resp));
7e21da6e
DM
225 }
226 };
227
e82dad97 228 match (info.handler)(parts, req_body, params, info, &mut rpcenv) {
0ee0ad5b
DM
229 Ok(future) => future,
230 Err(err) => {
6049b71f 231 let resp = (formatter.format_error)(Error::from(err));
0ee0ad5b
DM
232 Box::new(future::ok(resp))
233 }
234 }
7e21da6e
DM
235}
236
f4c514c1
DM
237fn get_index() -> BoxFut {
238
d15009c0
DM
239 let nodename = tools::nodename();
240 let username = "fakelogin"; // todo: implement real auth
f4c514c1
DM
241 let token = "abc";
242
243 let setup = json!({
244 "Setup": { "auth_cookie_name": "PBSAuthCookie" },
245 "NodeName": nodename,
246 "UserName": username,
d15009c0 247 "CSRFPreventionToken": token,
f4c514c1
DM
248 });
249
250 let index = format!(r###"
251<!DOCTYPE html>
252<html>
253 <head>
254 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
255 <meta http-equiv="X-UA-Compatible" content="IE=edge">
256 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
257 <title>Proxmox Backup Server</title>
8adbdb0a 258 <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
f4c514c1 259 <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
8adbdb0a
DM
260 <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
261 <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
f4c514c1
DM
262 <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
263 <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
8adbdb0a
DM
264 <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
265 <script type="text/javascript" src="/extjs/charts-debug.js"></script>
f4c514c1
DM
266 <script type="text/javascript">
267 Proxmox = {};
268 </script>
8adbdb0a
DM
269 <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
270 <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
f4c514c1
DM
271 <script type="text/javascript">
272 Ext.History.fieldid = 'x-history-field';
273 </script>
5c7a1b15 274 <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
f4c514c1
DM
275 </head>
276 <body>
277 <!-- Fields required for history management -->
278 <form id="history-form" class="x-hidden">
279 <input type="hidden" id="x-history-field"/>
280 </form>
281 </body>
282</html>
283"###, setup.to_string());
284
d15009c0
DM
285 let resp = Response::builder()
286 .status(StatusCode::OK)
287 .header(header::CONTENT_TYPE, "text/html")
288 // emulate succssful login, so that Proxmox:Utils.authOk() returns true
289 .header(header::SET_COOKIE, "PBSAuthCookie=\"XXX\"") // fixme: remove
290 .body(index.into())
291 .unwrap();
292
293 Box::new(future::ok(resp))
f4c514c1
DM
294}
295
826bb982
DM
296fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
297
298 if let Some(ext) = filename.extension().and_then(|osstr| osstr.to_str()) {
299 return match ext {
300 "css" => ("text/css", false),
301 "html" => ("text/html", false),
302 "js" => ("application/javascript", false),
303 "json" => ("application/json", false),
304 "map" => ("application/json", false),
305 "png" => ("image/png", true),
306 "ico" => ("image/x-icon", true),
307 "gif" => ("image/gif", true),
308 "svg" => ("image/svg+xml", false),
309 "jar" => ("application/java-archive", true),
310 "woff" => ("application/font-woff", true),
311 "woff2" => ("application/font-woff2", true),
312 "ttf" => ("application/font-snft", true),
313 "pdf" => ("application/pdf", true),
314 "epub" => ("application/epub+zip", true),
315 "mp3" => ("audio/mpeg", true),
316 "oga" => ("audio/ogg", true),
317 "tgz" => ("application/x-compressed-tar", true),
318 _ => ("application/octet-stream", false),
319 };
320 }
321
322 ("application/octet-stream", false)
323}
324
9bc17e8d
DM
325fn simple_static_file_download(filename: PathBuf) -> BoxFut {
326
826bb982
DM
327 let (content_type, _nocomp) = extension_to_content_type(&filename);
328
9bc17e8d 329 Box::new(File::open(filename)
4dcf43d2 330 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
826bb982 331 .and_then(move |file| {
9bc17e8d
DM
332 let buf: Vec<u8> = Vec::new();
333 tokio::io::read_to_end(file, buf)
4dcf43d2 334 .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err)))
826bb982
DM
335 .and_then(move |data| {
336 let mut response = Response::new(data.1.into());
337 response.headers_mut().insert(
338 header::CONTENT_TYPE,
339 header::HeaderValue::from_static(content_type));
340 Ok(response)
341 })
9bc17e8d
DM
342 }))
343}
344
345fn chuncked_static_file_download(filename: PathBuf) -> BoxFut {
346
826bb982
DM
347 let (content_type, _nocomp) = extension_to_content_type(&filename);
348
9bc17e8d 349 Box::new(File::open(filename)
4dcf43d2 350 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
826bb982 351 .and_then(move |file| {
059ca7c3 352 let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()).
9bc17e8d
DM
353 map(|bytes| {
354 //sigh - howto avoid copy here? or the whole map() ??
355 hyper::Chunk::from(bytes.to_vec())
356 });
357 let body = Body::wrap_stream(payload);
826bb982
DM
358
359 // fixme: set other headers ?
9bc17e8d
DM
360 Ok(Response::builder()
361 .status(StatusCode::OK)
826bb982 362 .header(header::CONTENT_TYPE, content_type)
9bc17e8d
DM
363 .body(body)
364 .unwrap())
365 }))
366}
367
368fn handle_static_file_download(filename: PathBuf) -> BoxFut {
369
370 let response = tokio::fs::metadata(filename.clone())
4dcf43d2 371 .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err)))
9bc17e8d
DM
372 .and_then(|metadata| {
373 if metadata.len() < 1024*32 {
374 Either::A(simple_static_file_download(filename))
375 } else {
376 Either::B(chuncked_static_file_download(filename))
377 }
378 });
379
380 return Box::new(response);
381}
382
383pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
384
385 let (parts, body) = req.into_parts();
386
387 let method = parts.method.clone();
388 let path = parts.uri.path();
389
390 // normalize path
391 // do not allow ".", "..", or hidden files ".XXXX"
392 // also remove empty path components
393
394 let items = path.split('/');
395 let mut path = String::new();
396 let mut components = vec![];
397
398 for name in items {
399 if name.is_empty() { continue; }
400 if name.starts_with(".") {
10be1d29 401 return Box::new(future::err(http_err!(BAD_REQUEST, "Path contains illegal components.".to_string())));
9bc17e8d
DM
402 }
403 path.push('/');
404 path.push_str(name);
405 components.push(name);
406 }
407
408 let comp_len = components.len();
409
410 println!("REQUEST {} {}", method, path);
411 println!("COMPO {:?}", components);
412
f1204833
DM
413 let env_type = api.env_type();
414 let mut rpcenv = RestEnvironment::new(env_type);
e82dad97 415
576e3bf2 416 if comp_len >= 1 && components[0] == "api2" {
9bc17e8d
DM
417 println!("GOT API REQUEST");
418 if comp_len >= 2 {
419 let format = components[1];
1571873d
DM
420 let formatter = match format {
421 "json" => &JSON_FORMATTER,
422 "extjs" => &EXTJS_FORMATTER,
423 _ => {
424 return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format))));
425 }
426 };
9bc17e8d 427
e7ea17de
DM
428 let mut uri_param = HashMap::new();
429
7e21da6e 430 // fixme: handle auth
d7d23785
DM
431 rpcenv.set_user(Some(String::from("root@pam")));
432
7e21da6e
DM
433 match api.find_method(&components[2..], method, &mut uri_param) {
434 MethodDefinition::None => {}
435 MethodDefinition::Simple(api_method) => {
f1204833
DM
436 if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
437 return proxy_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
438 } else {
439 return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
440 }
7e21da6e 441 }
50cfb695 442 MethodDefinition::Async(async_method) => {
e82dad97 443 return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param);
7e21da6e 444 }
9bc17e8d
DM
445 }
446 }
447 } else {
448 // not Auth for accessing files!
449
f4c514c1
DM
450 if comp_len == 0 {
451 return get_index();
452 } else {
453 let filename = api.find_alias(&components);
454 return handle_static_file_download(filename);
455 }
9bc17e8d
DM
456 }
457
10be1d29 458 Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string())))
9bc17e8d 459}