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