]>
Commit | Line | Data |
---|---|---|
f17db0ab DM |
1 | use crate::api::schema::*; |
2 | use crate::api::router::*; | |
3 | use crate::api::config::*; | |
16b48b81 | 4 | |
4dcf43d2 | 5 | use std::fmt; |
1a53be14 | 6 | use std::path::{PathBuf}; |
9bc17e8d | 7 | use std::sync::Arc; |
e7ea17de | 8 | use std::collections::HashMap; |
9bc17e8d DM |
9 | |
10 | use failure::*; | |
f4c514c1 | 11 | use serde_json::{json, Value}; |
e7ea17de | 12 | use url::form_urlencoded; |
9bc17e8d | 13 | |
9bc17e8d DM |
14 | use futures::future::{self, Either}; |
15 | //use tokio::prelude::*; | |
16 | //use tokio::timer::Delay; | |
17 | use tokio::fs::File; | |
18 | use tokio_codec; | |
19 | //use bytes::{BytesMut, BufMut}; | |
20 | ||
21 | //use hyper::body::Payload; | |
22 | use hyper::http::request::Parts; | |
10be1d29 | 23 | use hyper::{Body, Request, Response, StatusCode}; |
9bc17e8d DM |
24 | use hyper::service::{Service, NewService}; |
25 | use hyper::rt::{Future, Stream}; | |
26 | use hyper::header; | |
27 | ||
f0b10921 DM |
28 | pub struct RestServer { |
29 | pub api_config: Arc<ApiConfig>, | |
30 | } | |
31 | ||
32 | impl RestServer { | |
33 | ||
34 | pub fn new(api_config: ApiConfig) -> Self { | |
35 | Self { api_config: Arc::new(api_config) } | |
36 | } | |
37 | } | |
38 | ||
39 | impl NewService for RestServer | |
40 | { | |
41 | type ReqBody = Body; | |
42 | type ResBody = Body; | |
43 | type Error = hyper::Error; | |
44 | type InitError = hyper::Error; | |
45 | type Service = ApiService; | |
46 | type Future = Box<Future<Item = Self::Service, Error = Self::InitError> + Send>; | |
47 | fn new_service(&self) -> Self::Future { | |
48 | Box::new(future::ok(ApiService { api_config: self.api_config.clone() })) | |
49 | } | |
50 | } | |
51 | ||
52 | pub struct ApiService { | |
53 | pub api_config: Arc<ApiConfig>, | |
54 | } | |
55 | ||
56 | ||
57 | impl Service for ApiService { | |
58 | type ReqBody = Body; | |
59 | type ResBody = Body; | |
60 | type Error = hyper::Error; | |
61 | type Future = Box<Future<Item = Response<Body>, Error = Self::Error> + Send>; | |
62 | ||
63 | fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future { | |
64 | ||
65 | Box::new(handle_request(self.api_config.clone(), req).then(|result| { | |
66 | match result { | |
67 | Ok(res) => Ok::<_, hyper::Error>(res), | |
68 | Err(err) => { | |
4dcf43d2 | 69 | if let Some(apierr) = err.downcast_ref::<HttpError>() { |
f0b10921 DM |
70 | let mut resp = Response::new(Body::from(apierr.message.clone())); |
71 | *resp.status_mut() = apierr.code; | |
72 | Ok(resp) | |
73 | } else { | |
74 | let mut resp = Response::new(Body::from(err.to_string())); | |
75 | *resp.status_mut() = StatusCode::BAD_REQUEST; | |
76 | Ok(resp) | |
77 | } | |
78 | } | |
79 | } | |
80 | })) | |
81 | } | |
82 | } | |
83 | ||
9bc17e8d DM |
84 | type BoxFut = Box<Future<Item = Response<Body>, Error = failure::Error> + Send>; |
85 | ||
4dcf43d2 DM |
86 | #[derive(Debug, Fail)] |
87 | pub struct HttpError { | |
88 | pub code: StatusCode, | |
89 | pub message: String, | |
90 | } | |
91 | ||
92 | impl HttpError { | |
93 | pub fn new(code: StatusCode, message: String) -> Self { | |
94 | HttpError { code, message } | |
95 | } | |
96 | } | |
97 | ||
98 | impl 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 | ||
104 | macro_rules! http_err { | |
105 | ($status:ident, $msg:expr) => {{ | |
106 | Error::from(HttpError::new(StatusCode::$status, $msg)) | |
107 | }} | |
108 | } | |
109 | ||
279ecfdf DM |
110 | fn 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 | ||
128 | let bytes = String::from_utf8(body.to_vec())?; // why copy?? | |
129 | ||
130 | println!("GOT BODY {:?}", bytes); | |
131 | ||
e7ea17de | 132 | let mut param_list: Vec<(String, String)> = vec![]; |
9bc17e8d DM |
133 | |
134 | if bytes.len() > 0 { | |
e7ea17de DM |
135 | for (k, v) in form_urlencoded::parse(bytes.as_bytes()).into_owned() { |
136 | param_list.push((k, v)); | |
137 | } | |
138 | ||
9bc17e8d DM |
139 | } |
140 | ||
141 | if let Some(query_str) = parts.uri.query() { | |
e7ea17de DM |
142 | for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() { |
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, | |
9bc17e8d DM |
162 | parts: Parts, |
163 | req_body: Body, | |
e7ea17de | 164 | uri_param: HashMap<String, String>, |
279ecfdf | 165 | ) -> BoxFut |
9bc17e8d | 166 | { |
e7ea17de | 167 | let params = get_request_parameters_async(info, parts, req_body, uri_param); |
9bc17e8d DM |
168 | |
169 | let resp = params | |
170 | .and_then(move |params| { | |
171 | ||
172 | println!("GOT PARAMS {}", params); | |
173 | ||
174 | /* | |
175 | let when = Instant::now() + Duration::from_millis(3000); | |
176 | let task = Delay::new(when).then(|_| { | |
177 | println!("A LAZY TASK"); | |
178 | ok(()) | |
179 | }); | |
180 | ||
181 | tokio::spawn(task); | |
182 | */ | |
183 | ||
c548fa25 | 184 | let res = (info.handler)(params, info)?; |
9bc17e8d DM |
185 | |
186 | Ok(res) | |
187 | ||
188 | }).then(|result| { | |
189 | match result { | |
190 | Ok(ref value) => { | |
191 | let json_str = value.to_string(); | |
192 | ||
193 | Ok(Response::builder() | |
194 | .status(StatusCode::OK) | |
195 | .header(header::CONTENT_TYPE, "application/json") | |
196 | .body(Body::from(json_str))?) | |
197 | } | |
10be1d29 | 198 | Err(err) => Err(http_err!(BAD_REQUEST, err.to_string())) |
9bc17e8d DM |
199 | } |
200 | }); | |
201 | ||
202 | Box::new(resp) | |
203 | } | |
204 | ||
f4c514c1 DM |
205 | fn get_index() -> BoxFut { |
206 | ||
207 | let nodename = "unknown"; | |
208 | let username = ""; | |
209 | let token = "abc"; | |
210 | ||
211 | let setup = json!({ | |
212 | "Setup": { "auth_cookie_name": "PBSAuthCookie" }, | |
213 | "NodeName": nodename, | |
214 | "UserName": username, | |
215 | "CSRFPreventionToken": token | |
216 | }); | |
217 | ||
218 | let index = format!(r###" | |
219 | <!DOCTYPE html> | |
220 | <html> | |
221 | <head> | |
222 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
223 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
224 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | |
225 | <title>Proxmox Backup Server</title> | |
226 | <link rel="icon" sizes="128x128" href="/pve2/images/logo-128.png" /> | |
227 | <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" /> | |
228 | <link rel="stylesheet" type="text/css" href="/pve2/ext6/theme-crisp/resources/theme-crisp-all.css" /> | |
229 | <link rel="stylesheet" type="text/css" href="/pve2/ext6/crisp/resources/charts-all.css" /> | |
230 | <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" /> | |
231 | <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script> | |
232 | <script type="text/javascript" src="/pve2/ext6/ext-all-debug.js"></script> | |
233 | <script type="text/javascript" src="/pve2/ext6/charts-debug.js"></script> | |
234 | <script type="text/javascript"> | |
235 | Proxmox = {}; | |
236 | </script> | |
237 | <script type="text/javascript" src="/proxmoxlib.js"></script> | |
238 | <script type="text/javascript" src="/pve2/ext6/locale/locale-en.js"></script> | |
239 | <script type="text/javascript"> | |
240 | Ext.History.fieldid = 'x-history-field'; | |
241 | </script> | |
242 | <script type="text/javascript" src="/pve2/js/pbsmanagerlib.js"></script> | |
243 | </head> | |
244 | <body> | |
245 | <!-- Fields required for history management --> | |
246 | <form id="history-form" class="x-hidden"> | |
247 | <input type="hidden" id="x-history-field"/> | |
248 | </form> | |
249 | </body> | |
250 | </html> | |
251 | "###, setup.to_string()); | |
252 | ||
253 | Box::new(future::ok(Response::new(index.into()))) | |
254 | } | |
255 | ||
9bc17e8d DM |
256 | fn simple_static_file_download(filename: PathBuf) -> BoxFut { |
257 | ||
258 | Box::new(File::open(filename) | |
4dcf43d2 | 259 | .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err))) |
9bc17e8d DM |
260 | .and_then(|file| { |
261 | let buf: Vec<u8> = Vec::new(); | |
262 | tokio::io::read_to_end(file, buf) | |
4dcf43d2 | 263 | .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err))) |
9bc17e8d DM |
264 | .and_then(|data| Ok(Response::new(data.1.into()))) |
265 | })) | |
266 | } | |
267 | ||
268 | fn chuncked_static_file_download(filename: PathBuf) -> BoxFut { | |
269 | ||
270 | Box::new(File::open(filename) | |
4dcf43d2 | 271 | .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err))) |
9bc17e8d DM |
272 | .and_then(|file| { |
273 | let payload = tokio_codec::FramedRead::new(file, tokio_codec::BytesCodec::new()). | |
274 | map(|bytes| { | |
275 | //sigh - howto avoid copy here? or the whole map() ?? | |
276 | hyper::Chunk::from(bytes.to_vec()) | |
277 | }); | |
278 | let body = Body::wrap_stream(payload); | |
279 | // fixme: set content type and other headers | |
280 | Ok(Response::builder() | |
281 | .status(StatusCode::OK) | |
282 | .body(body) | |
283 | .unwrap()) | |
284 | })) | |
285 | } | |
286 | ||
287 | fn handle_static_file_download(filename: PathBuf) -> BoxFut { | |
288 | ||
289 | let response = tokio::fs::metadata(filename.clone()) | |
4dcf43d2 | 290 | .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err))) |
9bc17e8d DM |
291 | .and_then(|metadata| { |
292 | if metadata.len() < 1024*32 { | |
293 | Either::A(simple_static_file_download(filename)) | |
294 | } else { | |
295 | Either::B(chuncked_static_file_download(filename)) | |
296 | } | |
297 | }); | |
298 | ||
299 | return Box::new(response); | |
300 | } | |
301 | ||
302 | pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut { | |
303 | ||
304 | let (parts, body) = req.into_parts(); | |
305 | ||
306 | let method = parts.method.clone(); | |
307 | let path = parts.uri.path(); | |
308 | ||
309 | // normalize path | |
310 | // do not allow ".", "..", or hidden files ".XXXX" | |
311 | // also remove empty path components | |
312 | ||
313 | let items = path.split('/'); | |
314 | let mut path = String::new(); | |
315 | let mut components = vec![]; | |
316 | ||
317 | for name in items { | |
318 | if name.is_empty() { continue; } | |
319 | if name.starts_with(".") { | |
10be1d29 | 320 | return Box::new(future::err(http_err!(BAD_REQUEST, "Path contains illegal components.".to_string()))); |
9bc17e8d DM |
321 | } |
322 | path.push('/'); | |
323 | path.push_str(name); | |
324 | components.push(name); | |
325 | } | |
326 | ||
327 | let comp_len = components.len(); | |
328 | ||
329 | println!("REQUEST {} {}", method, path); | |
330 | println!("COMPO {:?}", components); | |
331 | ||
332 | if comp_len >= 1 && components[0] == "api3" { | |
333 | println!("GOT API REQUEST"); | |
334 | if comp_len >= 2 { | |
335 | let format = components[1]; | |
336 | if format != "json" { | |
10be1d29 | 337 | return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format)))) |
9bc17e8d DM |
338 | } |
339 | ||
e7ea17de DM |
340 | let mut uri_param = HashMap::new(); |
341 | ||
342 | if let Some(api_method) = api.find_method(&components[2..], method, &mut uri_param) { | |
9bc17e8d | 343 | // fixme: handle auth |
e7ea17de | 344 | return handle_sync_api_request(api_method, parts, body, uri_param); |
9bc17e8d DM |
345 | } |
346 | } | |
347 | } else { | |
348 | // not Auth for accessing files! | |
349 | ||
f4c514c1 DM |
350 | if comp_len == 0 { |
351 | return get_index(); | |
352 | } else { | |
353 | let filename = api.find_alias(&components); | |
354 | return handle_static_file_download(filename); | |
355 | } | |
9bc17e8d DM |
356 | } |
357 | ||
10be1d29 | 358 | Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string()))) |
9bc17e8d | 359 | } |