]> git.proxmox.com Git - proxmox-backup.git/blob - src/server/rest.rs
ccadae4736f5a0884bde9e171040455d6749e77c
[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 super::environment::RestEnvironment;
6 use super::formatter::*;
7
8 use std::fmt;
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 pub struct RestServer {
31 pub api_config: Arc<ApiConfig>,
32 }
33
34 impl RestServer {
35
36 pub fn new(api_config: ApiConfig) -> Self {
37 Self { api_config: Arc::new(api_config) }
38 }
39 }
40
41 impl 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
54 pub struct ApiService {
55 pub api_config: Arc<ApiConfig>,
56 }
57
58
59 impl 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) => {
71 if let Some(apierr) = err.downcast_ref::<HttpError>() {
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
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
110 fn get_request_parameters_async(
111 info: &'static ApiMethod,
112 parts: Parts,
113 req_body: Body,
114 uri_param: HashMap<String, String>,
115 ) -> Box<Future<Item = Value, Error = failure::Error> + Send>
116 {
117 let resp = req_body
118 .map_err(|err| http_err!(BAD_REQUEST, format!("Promlems reading request body: {}", err)))
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 }
124 else { Err(http_err!(BAD_REQUEST, format!("Request body too large"))) }
125 })
126 .and_then(move |body| {
127
128 let utf8 = std::str::from_utf8(&body)?;
129
130 println!("GOT BODY {:?}", utf8);
131
132 let mut param_list: Vec<(String, String)> = vec![];
133
134 if utf8.len() > 0 {
135 for (k, v) in form_urlencoded::parse(utf8.as_bytes()).into_owned() {
136 param_list.push((k, v));
137 }
138
139 }
140
141 if let Some(query_str) = parts.uri.query() {
142 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
143 if k == "_dc" { continue; } // skip extjs "disable cache" parameter
144 param_list.push((k, v));
145 }
146 }
147
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
154 println!("GOT PARAMS {}", params);
155 Ok(params)
156 });
157
158 Box::new(resp)
159 }
160
161 fn handle_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 let params = get_request_parameters_async(info, parts, req_body, uri_param);
171
172 let resp = params
173 .and_then(move |params| {
174 let resp = match (info.handler)(params, info, &mut rpcenv) {
175 Ok(data) => (formatter.format_result)(data, &rpcenv),
176 Err(err) => (formatter.format_error)(err),
177 };
178 Ok(resp)
179 });
180
181 Box::new(resp)
182 }
183
184 fn handle_async_api_request(
185 mut rpcenv: RestEnvironment,
186 info: &'static ApiAsyncMethod,
187 formatter: &'static OutputFormatter,
188 parts: Parts,
189 req_body: Body,
190 uri_param: HashMap<String, String>,
191 ) -> BoxFut
192 {
193 // fixme: convert parameters to Json
194 let mut param_list: Vec<(String, String)> = vec![];
195
196 if let Some(query_str) = parts.uri.query() {
197 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
198 if k == "_dc" { continue; } // skip extjs "disable cache" parameter
199 param_list.push((k, v));
200 }
201 }
202
203 for (k, v) in uri_param {
204 param_list.push((k.clone(), v.clone()));
205 }
206
207 let params = match parse_parameter_strings(&param_list, &info.parameters, true) {
208 Ok(v) => v,
209 Err(err) => {
210 let resp = (formatter.format_error)(Error::from(err));
211 return Box::new(future::ok(resp));
212 }
213 };
214
215 match (info.handler)(parts, req_body, params, info, &mut rpcenv) {
216 Ok(future) => future,
217 Err(err) => {
218 let resp = (formatter.format_error)(Error::from(err));
219 Box::new(future::ok(resp))
220 }
221 }
222 }
223
224 fn get_index() -> BoxFut {
225
226 let nodename = tools::nodename();
227 let username = "fakelogin"; // todo: implement real auth
228 let token = "abc";
229
230 let setup = json!({
231 "Setup": { "auth_cookie_name": "PBSAuthCookie" },
232 "NodeName": nodename,
233 "UserName": username,
234 "CSRFPreventionToken": token,
235 });
236
237 let index = format!(r###"
238 <!DOCTYPE html>
239 <html>
240 <head>
241 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
242 <meta http-equiv="X-UA-Compatible" content="IE=edge">
243 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
244 <title>Proxmox Backup Server</title>
245 <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
246 <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
247 <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
248 <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
249 <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
250 <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
251 <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
252 <script type="text/javascript" src="/extjs/charts-debug.js"></script>
253 <script type="text/javascript">
254 Proxmox = {};
255 </script>
256 <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
257 <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
258 <script type="text/javascript">
259 Ext.History.fieldid = 'x-history-field';
260 </script>
261 <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
262 </head>
263 <body>
264 <!-- Fields required for history management -->
265 <form id="history-form" class="x-hidden">
266 <input type="hidden" id="x-history-field"/>
267 </form>
268 </body>
269 </html>
270 "###, setup.to_string());
271
272 let resp = Response::builder()
273 .status(StatusCode::OK)
274 .header(header::CONTENT_TYPE, "text/html")
275 // emulate succssful login, so that Proxmox:Utils.authOk() returns true
276 .header(header::SET_COOKIE, "PBSAuthCookie=\"XXX\"") // fixme: remove
277 .body(index.into())
278 .unwrap();
279
280 Box::new(future::ok(resp))
281 }
282
283 fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
284
285 if let Some(ext) = filename.extension().and_then(|osstr| osstr.to_str()) {
286 return match ext {
287 "css" => ("text/css", false),
288 "html" => ("text/html", false),
289 "js" => ("application/javascript", false),
290 "json" => ("application/json", false),
291 "map" => ("application/json", false),
292 "png" => ("image/png", true),
293 "ico" => ("image/x-icon", true),
294 "gif" => ("image/gif", true),
295 "svg" => ("image/svg+xml", false),
296 "jar" => ("application/java-archive", true),
297 "woff" => ("application/font-woff", true),
298 "woff2" => ("application/font-woff2", true),
299 "ttf" => ("application/font-snft", true),
300 "pdf" => ("application/pdf", true),
301 "epub" => ("application/epub+zip", true),
302 "mp3" => ("audio/mpeg", true),
303 "oga" => ("audio/ogg", true),
304 "tgz" => ("application/x-compressed-tar", true),
305 _ => ("application/octet-stream", false),
306 };
307 }
308
309 ("application/octet-stream", false)
310 }
311
312 fn simple_static_file_download(filename: PathBuf) -> BoxFut {
313
314 let (content_type, _nocomp) = extension_to_content_type(&filename);
315
316 Box::new(File::open(filename)
317 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
318 .and_then(move |file| {
319 let buf: Vec<u8> = Vec::new();
320 tokio::io::read_to_end(file, buf)
321 .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err)))
322 .and_then(move |data| {
323 let mut response = Response::new(data.1.into());
324 response.headers_mut().insert(
325 header::CONTENT_TYPE,
326 header::HeaderValue::from_static(content_type));
327 Ok(response)
328 })
329 }))
330 }
331
332 fn chuncked_static_file_download(filename: PathBuf) -> BoxFut {
333
334 let (content_type, _nocomp) = extension_to_content_type(&filename);
335
336 Box::new(File::open(filename)
337 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
338 .and_then(move |file| {
339 let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()).
340 map(|bytes| {
341 //sigh - howto avoid copy here? or the whole map() ??
342 hyper::Chunk::from(bytes.to_vec())
343 });
344 let body = Body::wrap_stream(payload);
345
346 // fixme: set other headers ?
347 Ok(Response::builder()
348 .status(StatusCode::OK)
349 .header(header::CONTENT_TYPE, content_type)
350 .body(body)
351 .unwrap())
352 }))
353 }
354
355 fn handle_static_file_download(filename: PathBuf) -> BoxFut {
356
357 let response = tokio::fs::metadata(filename.clone())
358 .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err)))
359 .and_then(|metadata| {
360 if metadata.len() < 1024*32 {
361 Either::A(simple_static_file_download(filename))
362 } else {
363 Either::B(chuncked_static_file_download(filename))
364 }
365 });
366
367 return Box::new(response);
368 }
369
370 pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
371
372 let (parts, body) = req.into_parts();
373
374 let method = parts.method.clone();
375 let path = parts.uri.path();
376
377 // normalize path
378 // do not allow ".", "..", or hidden files ".XXXX"
379 // also remove empty path components
380
381 let items = path.split('/');
382 let mut path = String::new();
383 let mut components = vec![];
384
385 for name in items {
386 if name.is_empty() { continue; }
387 if name.starts_with(".") {
388 return Box::new(future::err(http_err!(BAD_REQUEST, "Path contains illegal components.".to_string())));
389 }
390 path.push('/');
391 path.push_str(name);
392 components.push(name);
393 }
394
395 let comp_len = components.len();
396
397 println!("REQUEST {} {}", method, path);
398 println!("COMPO {:?}", components);
399
400 let mut rpcenv = RestEnvironment::new(RpcEnvironmentType::PRIVILEDGED);
401
402 if comp_len >= 1 && components[0] == "api2" {
403 println!("GOT API REQUEST");
404 if comp_len >= 2 {
405 let format = components[1];
406 let formatter = match format {
407 "json" => &JSON_FORMATTER,
408 "extjs" => &EXTJS_FORMATTER,
409 _ => {
410 return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format))));
411 }
412 };
413
414 let mut uri_param = HashMap::new();
415
416 // fixme: handle auth
417 rpcenv.set_user(Some(String::from("root@pam")));
418
419 match api.find_method(&components[2..], method, &mut uri_param) {
420 MethodDefinition::None => {}
421 MethodDefinition::Simple(api_method) => {
422 return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
423 }
424 MethodDefinition::Async(async_method) => {
425 return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param);
426 }
427 }
428 }
429 } else {
430 // not Auth for accessing files!
431
432 if comp_len == 0 {
433 return get_index();
434 } else {
435 let filename = api.find_alias(&components);
436 return handle_static_file_download(filename);
437 }
438 }
439
440 Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string())))
441 }