1 use crate::api
::schema
::*;
2 use crate::api
::router
::*;
3 use crate::api
::config
::*;
4 use super::formatter
::*;
7 use std
::path
::{Path, PathBuf}
;
9 use std
::collections
::HashMap
;
12 use serde_json
::{json, Value}
;
13 use url
::form_urlencoded
;
15 use futures
::future
::{self, Either}
;
16 //use tokio::prelude::*;
17 //use tokio::timer::Delay;
19 //use bytes::{BytesMut, BufMut};
21 //use hyper::body::Payload;
22 use hyper
::http
::request
::Parts
;
23 use hyper
::{Body, Request, Response, StatusCode}
;
24 use hyper
::service
::{Service, NewService}
;
25 use hyper
::rt
::{Future, Stream}
;
28 pub struct RestServer
{
29 pub api_config
: Arc
<ApiConfig
>,
34 pub fn new(api_config
: ApiConfig
) -> Self {
35 Self { api_config: Arc::new(api_config) }
39 impl NewService
for RestServer
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() }
))
52 pub struct ApiService
{
53 pub api_config
: Arc
<ApiConfig
>,
57 impl Service
for ApiService
{
60 type Error
= hyper
::Error
;
61 type Future
= Box
<Future
<Item
= Response
<Body
>, Error
= Self::Error
> + Send
>;
63 fn call(&mut self, req
: Request
<Self::ReqBody
>) -> Self::Future
{
65 Box
::new(handle_request(self.api_config
.clone(), req
).then(|result
| {
67 Ok(res
) => Ok
::<_
, hyper
::Error
>(res
),
69 if let Some(apierr
) = err
.downcast_ref
::<HttpError
>() {
70 let mut resp
= Response
::new(Body
::from(apierr
.message
.clone()));
71 *resp
.status_mut() = apierr
.code
;
74 let mut resp
= Response
::new(Body
::from(err
.to_string()));
75 *resp
.status_mut() = StatusCode
::BAD_REQUEST
;
84 #[derive(Debug, Fail)]
85 pub struct HttpError
{
91 pub fn new(code
: StatusCode
, message
: String
) -> Self {
92 HttpError { code, message }
96 impl fmt
::Display
for HttpError
{
97 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
98 write
!(f
, "Error {}: {}", self.code
, self.message
)
102 macro_rules
! http_err
{
103 ($status
:ident
, $msg
:expr
) => {{
104 Error
::from(HttpError
::new(StatusCode
::$status
, $msg
))
108 fn get_request_parameters_async(
109 info
: &'
static ApiMethod
,
112 uri_param
: HashMap
<String
, String
>,
113 ) -> Box
<Future
<Item
= Value
, Error
= failure
::Error
> + Send
>
116 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("Promlems reading request body: {}", err
)))
117 .fold(Vec
::new(), |mut acc
, chunk
| {
118 if acc
.len() + chunk
.len() < 64*1024 { //fimxe: max request body size?
119 acc
.extend_from_slice(&*chunk
);
122 else { Err(http_err!(BAD_REQUEST, format!("Request body too large"))) }
124 .and_then(move |body
| {
126 let utf8
= std
::str::from_utf8(&body
)?
;
128 println
!("GOT BODY {:?}", utf8
);
130 let mut param_list
: Vec
<(String
, String
)> = vec
![];
133 for (k
, v
) in form_urlencoded
::parse(utf8
.as_bytes()).into_owned() {
134 param_list
.push((k
, v
));
139 if let Some(query_str
) = parts
.uri
.query() {
140 for (k
, v
) in form_urlencoded
::parse(query_str
.as_bytes()).into_owned() {
141 if k
== "_dc" { continue; }
// skip extjs "disable cache" parameter
142 param_list
.push((k
, v
));
146 for (k
, v
) in uri_param
{
147 param_list
.push((k
.clone(), v
.clone()));
150 let params
= parse_parameter_strings(¶m_list
, &info
.parameters
, true)?
;
152 println
!("GOT PARAMS {}", params
);
159 fn handle_sync_api_request(
160 info
: &'
static ApiMethod
,
161 formatter
: &'
static OutputFormatter
,
164 uri_param
: HashMap
<String
, String
>,
167 let params
= get_request_parameters_async(info
, parts
, req_body
, uri_param
);
170 .and_then(move |params
| {
171 let res
= (info
.handler
)(params
, info
)?
;
173 }).then(move |result
| {
174 Ok((formatter
.format_result
)(result
))
180 fn handle_async_api_request(
181 info
: &'
static ApiAsyncMethod
,
182 formatter
: &'
static OutputFormatter
,
185 uri_param
: HashMap
<String
, String
>,
188 // fixme: convert parameters to Json
189 let mut param_list
: Vec
<(String
, String
)> = vec
![];
191 if let Some(query_str
) = parts
.uri
.query() {
192 for (k
, v
) in form_urlencoded
::parse(query_str
.as_bytes()).into_owned() {
193 if k
== "_dc" { continue; }
// skip extjs "disable cache" parameter
194 param_list
.push((k
, v
));
198 for (k
, v
) in uri_param
{
199 param_list
.push((k
.clone(), v
.clone()));
202 let params
= match parse_parameter_strings(¶m_list
, &info
.parameters
, true) {
205 let resp
= (formatter
.format_result
)(Err(Error
::from(err
)));
206 return Box
::new(future
::ok(resp
));
210 match (info
.handler
)(parts
, req_body
, params
, info
) {
211 Ok(future
) => future
,
213 let resp
= (formatter
.format_result
)(Err(Error
::from(err
)));
214 Box
::new(future
::ok(resp
))
219 fn get_index() -> BoxFut
{
221 let nodename
= "unknown";
226 "Setup": { "auth_cookie_name": "PBSAuthCookie" }
,
227 "NodeName": nodename
,
228 "UserName": username
,
229 "CSRFPreventionToken": token
232 let index
= format
!(r
###"
236 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
237 <meta http-equiv="X-UA-Compatible" content="IE=edge">
238 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
239 <title>Proxmox Backup Server</title>
240 <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
241 <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
242 <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
243 <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
244 <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
245 <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
246 <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
247 <script type="text/javascript" src="/extjs/charts-debug.js"></script>
248 <script type="text/javascript">
251 <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
252 <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
253 <script type="text/javascript">
254 Ext.History.fieldid = 'x-history-field';
256 <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
259 <!-- Fields required for history management -->
260 <form id="history-form" class="x-hidden">
261 <input type="hidden" id="x-history-field"/>
265 "###, setup.to_string());
267 Box
::new(future
::ok(Response
::new(index
.into())))
270 fn extension_to_content_type(filename
: &Path
) -> (&'
static str, bool
) {
272 if let Some(ext
) = filename
.extension().and_then(|osstr
| osstr
.to_str()) {
274 "css" => ("text/css", false),
275 "html" => ("text/html", false),
276 "js" => ("application/javascript", false),
277 "json" => ("application/json", false),
278 "map" => ("application/json", false),
279 "png" => ("image/png", true),
280 "ico" => ("image/x-icon", true),
281 "gif" => ("image/gif", true),
282 "svg" => ("image/svg+xml", false),
283 "jar" => ("application/java-archive", true),
284 "woff" => ("application/font-woff", true),
285 "woff2" => ("application/font-woff2", true),
286 "ttf" => ("application/font-snft", true),
287 "pdf" => ("application/pdf", true),
288 "epub" => ("application/epub+zip", true),
289 "mp3" => ("audio/mpeg", true),
290 "oga" => ("audio/ogg", true),
291 "tgz" => ("application/x-compressed-tar", true),
292 _
=> ("application/octet-stream", false),
296 ("application/octet-stream", false)
299 fn simple_static_file_download(filename
: PathBuf
) -> BoxFut
{
301 let (content_type
, _nocomp
) = extension_to_content_type(&filename
);
303 Box
::new(File
::open(filename
)
304 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File open failed: {}", err
)))
305 .and_then(move |file
| {
306 let buf
: Vec
<u8> = Vec
::new();
307 tokio
::io
::read_to_end(file
, buf
)
308 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File read failed: {}", err
)))
309 .and_then(move |data
| {
310 let mut response
= Response
::new(data
.1.into
());
311 response
.headers_mut().insert(
312 header
::CONTENT_TYPE
,
313 header
::HeaderValue
::from_static(content_type
));
319 fn chuncked_static_file_download(filename
: PathBuf
) -> BoxFut
{
321 let (content_type
, _nocomp
) = extension_to_content_type(&filename
);
323 Box
::new(File
::open(filename
)
324 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File open failed: {}", err
)))
325 .and_then(move |file
| {
326 let payload
= tokio
::codec
::FramedRead
::new(file
, tokio
::codec
::BytesCodec
::new()).
328 //sigh - howto avoid copy here? or the whole map() ??
329 hyper
::Chunk
::from(bytes
.to_vec())
331 let body
= Body
::wrap_stream(payload
);
333 // fixme: set other headers ?
334 Ok(Response
::builder()
335 .status(StatusCode
::OK
)
336 .header(header
::CONTENT_TYPE
, content_type
)
342 fn handle_static_file_download(filename
: PathBuf
) -> BoxFut
{
344 let response
= tokio
::fs
::metadata(filename
.clone())
345 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File access problems: {}", err
)))
346 .and_then(|metadata
| {
347 if metadata
.len() < 1024*32 {
348 Either
::A(simple_static_file_download(filename
))
350 Either
::B(chuncked_static_file_download(filename
))
354 return Box
::new(response
);
357 pub fn handle_request(api
: Arc
<ApiConfig
>, req
: Request
<Body
>) -> BoxFut
{
359 let (parts
, body
) = req
.into_parts();
361 let method
= parts
.method
.clone();
362 let path
= parts
.uri
.path();
365 // do not allow ".", "..", or hidden files ".XXXX"
366 // also remove empty path components
368 let items
= path
.split('
/'
);
369 let mut path
= String
::new();
370 let mut components
= vec
![];
373 if name
.is_empty() { continue; }
374 if name
.starts_with(".") {
375 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, "Path contains illegal components.".to_string())));
379 components
.push(name
);
382 let comp_len
= components
.len();
384 println
!("REQUEST {} {}", method
, path
);
385 println
!("COMPO {:?}", components
);
387 if comp_len
>= 1 && components
[0] == "api2" {
388 println
!("GOT API REQUEST");
390 let format
= components
[1];
391 let formatter
= match format
{
392 "json" => &JSON_FORMATTER
,
393 "extjs" => &EXTJS_FORMATTER
,
395 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, format
!("Unsupported output format '{}'.", format
))));
399 let mut uri_param
= HashMap
::new();
401 // fixme: handle auth
402 match api
.find_method(&components
[2..], method
, &mut uri_param
) {
403 MethodDefinition
::None
=> {}
404 MethodDefinition
::Simple(api_method
) => {
405 return handle_sync_api_request(api_method
, formatter
, parts
, body
, uri_param
);
407 MethodDefinition
::Async(async_method
) => {
408 return handle_async_api_request(async_method
, formatter
, parts
, body
, uri_param
);
413 // not Auth for accessing files!
418 let filename
= api
.find_alias(&components
);
419 return handle_static_file_download(filename
);
423 Box
::new(future
::err(http_err
!(NOT_FOUND
, "Path not found.".to_string())))