2 use crate::api_schema
::*;
3 use crate::api_schema
::router
::*;
4 use crate::api_schema
::config
::*;
5 use crate::auth_helpers
::*;
6 use super::environment
::RestEnvironment
;
7 use super::formatter
::*;
9 use std
::path
::{Path, PathBuf}
;
11 use std
::collections
::HashMap
;
14 use serde_json
::{json, Value}
;
15 use url
::form_urlencoded
;
17 use futures
::future
::{self, Either}
;
18 //use tokio::prelude::*;
19 //use tokio::timer::Delay;
21 //use bytes::{BytesMut, BufMut};
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}
;
30 extern "C" { fn tzset(); }
32 pub struct RestServer
{
33 pub api_config
: Arc
<ApiConfig
>,
38 pub fn new(api_config
: ApiConfig
) -> Self {
39 Self { api_config: Arc::new(api_config) }
43 impl NewService
for RestServer
47 type Error
= hyper
::Error
;
48 type InitError
= hyper
::Error
;
49 type Service
= ApiService
;
50 type Future
= Box
<dyn Future
<Item
= Self::Service
, Error
= Self::InitError
> + Send
>;
51 fn new_service(&self) -> Self::Future
{
52 Box
::new(future
::ok(ApiService { api_config: self.api_config.clone() }
))
56 pub struct ApiService
{
57 pub api_config
: Arc
<ApiConfig
>,
60 fn log_response(method
: hyper
::Method
, path
: &str, resp
: &Response
<Body
>) {
62 if resp
.extensions().get
::<NoLogExtension
>().is_some() { return; }
;
64 let status
= resp
.status();
66 if !(status
.is_success() || status
.is_informational()) {
67 let reason
= status
.canonical_reason().unwrap_or("unknown reason");
68 let client
= "unknown"; // fixme: howto get peer_addr ?
70 let mut message
= "request failed";
71 if let Some(data
) = resp
.extensions().get
::<ErrorMessageExtension
>() {
75 log
::error
!("{} {}: {} {}: [client {}] {}", method
.as_str(), path
, status
.as_str(), reason
, client
, message
);
79 impl Service
for ApiService
{
82 type Error
= hyper
::Error
;
83 type Future
= Box
<dyn Future
<Item
= Response
<Body
>, Error
= Self::Error
> + Send
>;
85 fn call(&mut self, req
: Request
<Self::ReqBody
>) -> Self::Future
{
86 let path
= req
.uri().path().to_owned();
87 let method
= req
.method().clone();
89 Box
::new(handle_request(self.api_config
.clone(), req
).then(move |result
| {
92 log_response(method
, &path
, &res
);
93 Ok
::<_
, hyper
::Error
>(res
)
96 if let Some(apierr
) = err
.downcast_ref
::<HttpError
>() {
97 let mut resp
= Response
::new(Body
::from(apierr
.message
.clone()));
98 *resp
.status_mut() = apierr
.code
;
99 log_response(method
, &path
, &resp
);
102 let mut resp
= Response
::new(Body
::from(err
.to_string()));
103 *resp
.status_mut() = StatusCode
::BAD_REQUEST
;
104 log_response(method
, &path
, &resp
);
113 fn get_request_parameters_async(
114 info
: &'
static ApiMethod
,
117 uri_param
: HashMap
<String
, String
>,
118 ) -> Box
<dyn Future
<Item
= Value
, Error
= failure
::Error
> + Send
>
120 let mut is_json
= false;
122 if let Some(value
) = parts
.headers
.get(header
::CONTENT_TYPE
) {
123 match value
.to_str().map(|v
| v
.split('
;'
).next()) {
124 Ok(Some("application/x-www-form-urlencoded")) => {
127 Ok(Some("application/json")) => {
131 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, format
!("unsupported content type"))));
137 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("Promlems reading request body: {}", err
)))
138 .fold(Vec
::new(), |mut acc
, chunk
| {
139 if acc
.len() + chunk
.len() < 64*1024 { //fimxe: max request body size?
140 acc
.extend_from_slice(&*chunk
);
143 else { Err(http_err!(BAD_REQUEST, format!("Request body too large"))) }
145 .and_then(move |body
| {
147 let utf8
= std
::str::from_utf8(&body
)?
;
149 let obj_schema
= &info
.parameters
;
152 let mut params
: Value
= serde_json
::from_str(utf8
)?
;
153 for (k
, v
) in uri_param
{
154 if let Some((_optional
, prop_schema
)) = obj_schema
.properties
.get
::<str>(&k
) {
155 params
[&k
] = parse_simple_value(&v
, prop_schema
)?
;
158 verify_json_object(¶ms
, obj_schema
)?
;
162 let mut param_list
: Vec
<(String
, String
)> = vec
![];
165 for (k
, v
) in form_urlencoded
::parse(utf8
.as_bytes()).into_owned() {
166 param_list
.push((k
, v
));
171 if let Some(query_str
) = parts
.uri
.query() {
172 for (k
, v
) in form_urlencoded
::parse(query_str
.as_bytes()).into_owned() {
173 if k
== "_dc" { continue; }
// skip extjs "disable cache" parameter
174 param_list
.push((k
, v
));
178 for (k
, v
) in uri_param
{
179 param_list
.push((k
.clone(), v
.clone()));
182 let params
= parse_parameter_strings(¶m_list
, obj_schema
, true)?
;
190 struct NoLogExtension();
192 fn proxy_protected_request(
193 info
: &'
static ApiMethod
,
199 let mut uri_parts
= parts
.uri
.clone().into_parts();
201 uri_parts
.scheme
= Some(http
::uri
::Scheme
::HTTP
);
202 uri_parts
.authority
= Some(http
::uri
::Authority
::from_static("127.0.0.1:82"));
203 let new_uri
= http
::Uri
::from_parts(uri_parts
).unwrap();
207 let request
= Request
::from_parts(parts
, req_body
);
209 let resp
= hyper
::client
::Client
::new()
211 .map_err(Error
::from
)
213 resp
.extensions_mut().insert(NoLogExtension());
218 let resp
= if info
.reload_timezone
{
219 Either
::A(resp
.then(|resp
| {unsafe { tzset() }
; resp
}))
224 return Box
::new(resp
);
227 pub fn handle_sync_api_request
<Env
: RpcEnvironment
>(
229 info
: &'
static ApiMethod
,
230 formatter
: &'
static OutputFormatter
,
233 uri_param
: HashMap
<String
, String
>,
236 let params
= get_request_parameters_async(info
, parts
, req_body
, uri_param
);
238 let delay_unauth_time
= std
::time
::Instant
::now() + std
::time
::Duration
::from_millis(3000);
241 .and_then(move |params
| {
242 let mut delay
= false;
243 let resp
= match (info
.handler
.as_ref().unwrap())(params
, info
, &mut rpcenv
) {
244 Ok(data
) => (formatter
.format_data
)(data
, &rpcenv
),
246 if let Some(httperr
) = err
.downcast_ref
::<HttpError
>() {
247 if httperr
.code
== StatusCode
::UNAUTHORIZED { delay = true; }
249 (formatter
.format_error
)(err
)
253 if info
.reload_timezone
{
258 Either
::A(delayed_response(resp
, delay_unauth_time
))
260 Either
::B(future
::ok(resp
))
263 .or_else(move |err
| {
264 Ok((formatter
.format_error
)(err
))
270 pub fn handle_async_api_request
<Env
: RpcEnvironment
>(
272 info
: &'
static ApiAsyncMethod
,
273 formatter
: &'
static OutputFormatter
,
276 uri_param
: HashMap
<String
, String
>,
279 // fixme: convert parameters to Json
280 let mut param_list
: Vec
<(String
, String
)> = vec
![];
282 if let Some(query_str
) = parts
.uri
.query() {
283 for (k
, v
) in form_urlencoded
::parse(query_str
.as_bytes()).into_owned() {
284 if k
== "_dc" { continue; }
// skip extjs "disable cache" parameter
285 param_list
.push((k
, v
));
289 for (k
, v
) in uri_param
{
290 param_list
.push((k
.clone(), v
.clone()));
293 let params
= match parse_parameter_strings(¶m_list
, &info
.parameters
, true) {
296 let resp
= (formatter
.format_error
)(Error
::from(err
));
297 return Box
::new(future
::ok(resp
));
301 match (info
.handler
)(parts
, req_body
, params
, info
, Box
::new(rpcenv
)) {
302 Ok(future
) => future
,
304 let resp
= (formatter
.format_error
)(Error
::from(err
));
305 Box
::new(future
::ok(resp
))
310 fn get_index(username
: Option
<String
>, token
: Option
<String
>) -> Response
<Body
> {
312 let nodename
= tools
::nodename();
313 let username
= username
.unwrap_or(String
::from(""));
315 let token
= token
.unwrap_or(String
::from(""));
318 "Setup": { "auth_cookie_name": "PBSAuthCookie" }
,
319 "NodeName": nodename
,
320 "UserName": username
,
321 "CSRFPreventionToken": token
,
324 let index
= format
!(r
###"
328 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
329 <meta http-equiv="X-UA-Compatible" content="IE=edge">
330 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
331 <title>Proxmox Backup Server</title>
332 <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
333 <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
334 <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
335 <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
336 <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
337 <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
338 <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
339 <script type="text/javascript" src="/extjs/charts-debug.js"></script>
340 <script type="text/javascript">
343 <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
344 <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
345 <script type="text/javascript">
346 Ext.History.fieldid = 'x-history-field';
348 <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
351 <!-- Fields required for history management -->
352 <form id="history-form" class="x-hidden">
353 <input type="hidden" id="x-history-field"/>
357 "###, setup.to_string());
360 .status(StatusCode
::OK
)
361 .header(header
::CONTENT_TYPE
, "text/html")
366 fn extension_to_content_type(filename
: &Path
) -> (&'
static str, bool
) {
368 if let Some(ext
) = filename
.extension().and_then(|osstr
| osstr
.to_str()) {
370 "css" => ("text/css", false),
371 "html" => ("text/html", false),
372 "js" => ("application/javascript", false),
373 "json" => ("application/json", false),
374 "map" => ("application/json", false),
375 "png" => ("image/png", true),
376 "ico" => ("image/x-icon", true),
377 "gif" => ("image/gif", true),
378 "svg" => ("image/svg+xml", false),
379 "jar" => ("application/java-archive", true),
380 "woff" => ("application/font-woff", true),
381 "woff2" => ("application/font-woff2", true),
382 "ttf" => ("application/font-snft", true),
383 "pdf" => ("application/pdf", true),
384 "epub" => ("application/epub+zip", true),
385 "mp3" => ("audio/mpeg", true),
386 "oga" => ("audio/ogg", true),
387 "tgz" => ("application/x-compressed-tar", true),
388 _
=> ("application/octet-stream", false),
392 ("application/octet-stream", false)
395 fn simple_static_file_download(filename
: PathBuf
) -> BoxFut
{
397 let (content_type
, _nocomp
) = extension_to_content_type(&filename
);
399 Box
::new(File
::open(filename
)
400 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File open failed: {}", err
)))
401 .and_then(move |file
| {
402 let buf
: Vec
<u8> = Vec
::new();
403 tokio
::io
::read_to_end(file
, buf
)
404 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File read failed: {}", err
)))
405 .and_then(move |data
| {
406 let mut response
= Response
::new(data
.1.into
());
407 response
.headers_mut().insert(
408 header
::CONTENT_TYPE
,
409 header
::HeaderValue
::from_static(content_type
));
415 fn chuncked_static_file_download(filename
: PathBuf
) -> BoxFut
{
417 let (content_type
, _nocomp
) = extension_to_content_type(&filename
);
419 Box
::new(File
::open(filename
)
420 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File open failed: {}", err
)))
421 .and_then(move |file
| {
422 let payload
= tokio
::codec
::FramedRead
::new(file
, tokio
::codec
::BytesCodec
::new()).
424 //sigh - howto avoid copy here? or the whole map() ??
425 hyper
::Chunk
::from(bytes
.to_vec())
427 let body
= Body
::wrap_stream(payload
);
429 // fixme: set other headers ?
430 Ok(Response
::builder()
431 .status(StatusCode
::OK
)
432 .header(header
::CONTENT_TYPE
, content_type
)
438 fn handle_static_file_download(filename
: PathBuf
) -> BoxFut
{
440 let response
= tokio
::fs
::metadata(filename
.clone())
441 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File access problems: {}", err
)))
442 .and_then(|metadata
| {
443 if metadata
.len() < 1024*32 {
444 Either
::A(simple_static_file_download(filename
))
446 Either
::B(chuncked_static_file_download(filename
))
450 return Box
::new(response
);
453 fn extract_auth_data(headers
: &http
::HeaderMap
) -> (Option
<String
>, Option
<String
>) {
455 let mut ticket
= None
;
456 if let Some(raw_cookie
) = headers
.get("COOKIE") {
457 if let Ok(cookie
) = raw_cookie
.to_str() {
458 ticket
= tools
::extract_auth_cookie(cookie
, "PBSAuthCookie");
462 let token
= match headers
.get("CSRFPreventionToken").map(|v
| v
.to_str()) {
463 Some(Ok(v
)) => Some(v
.to_owned()),
470 fn check_auth(method
: &hyper
::Method
, ticket
: &Option
<String
>, token
: &Option
<String
>) -> Result
<String
, Error
> {
472 let ticket_lifetime
= tools
::ticket
::TICKET_LIFETIME
;
474 let username
= match ticket
{
475 Some(ticket
) => match tools
::ticket
::verify_rsa_ticket(public_auth_key(), "PBS", &ticket
, None
, -300, ticket_lifetime
) {
476 Ok((_age
, Some(username
))) => username
.to_owned(),
477 Ok((_
, None
)) => bail
!("ticket without username."),
478 Err(err
) => return Err(err
),
480 None
=> bail
!("missing ticket"),
483 if method
!= hyper
::Method
::GET
{
484 if let Some(token
) = token
{
485 println
!("CSRF prevention token: {:?}", token
);
486 verify_csrf_prevention_token(csrf_secret(), &username
, &token
, -300, ticket_lifetime
)?
;
488 bail
!("missing CSRF prevention token");
495 fn delayed_response(resp
: Response
<Body
>, delay_unauth_time
: std
::time
::Instant
) -> BoxFut
{
497 Box
::new(tokio
::timer
::Delay
::new(delay_unauth_time
)
498 .map_err(|err
| http_err
!(INTERNAL_SERVER_ERROR
, format
!("tokio timer delay error: {}", err
)))
499 .and_then(|_
| Ok(resp
)))
502 pub fn handle_request(api
: Arc
<ApiConfig
>, req
: Request
<Body
>) -> BoxFut
{
504 let (parts
, body
) = req
.into_parts();
506 let method
= parts
.method
.clone();
508 let (path
, components
) = match tools
::normalize_uri_path(parts
.uri
.path()) {
510 Err(err
) => return Box
::new(future
::err(http_err
!(BAD_REQUEST
, err
.to_string()))),
513 let comp_len
= components
.len();
515 println
!("REQUEST {} {}", method
, path
);
516 println
!("COMPO {:?}", components
);
518 let env_type
= api
.env_type();
519 let mut rpcenv
= RestEnvironment
::new(env_type
);
521 let delay_unauth_time
= std
::time
::Instant
::now() + std
::time
::Duration
::from_millis(3000);
523 if comp_len
>= 1 && components
[0] == "api2" {
526 let format
= components
[1];
527 let formatter
= match format
{
528 "json" => &JSON_FORMATTER
,
529 "extjs" => &EXTJS_FORMATTER
,
531 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, format
!("Unsupported output format '{}'.", format
))));
535 let mut uri_param
= HashMap
::new();
537 if comp_len
== 4 && components
[2] == "access" && components
[3] == "ticket" {
538 // explicitly allow those calls without auth
540 let (ticket
, token
) = extract_auth_data(&parts
.headers
);
541 match check_auth(&method
, &ticket
, &token
) {
544 // fixme: check permissions
546 rpcenv
.set_user(Some(username
));
549 // always delay unauthorized calls by 3 seconds (from start of request)
550 let err
= http_err
!(UNAUTHORIZED
, format
!("permission check failed - {}", err
));
551 return delayed_response((formatter
.format_error
)(err
), delay_unauth_time
);
556 match api
.find_method(&components
[2..], method
, &mut uri_param
) {
557 MethodDefinition
::None
=> {
558 let err
= http_err
!(NOT_FOUND
, "Path not found.".to_string());
559 return Box
::new(future
::ok((formatter
.format_error
)(err
)));
561 MethodDefinition
::Simple(api_method
) => {
562 if api_method
.protected
&& env_type
== RpcEnvironmentType
::PUBLIC
{
563 return proxy_protected_request(api_method
, parts
, body
);
565 return handle_sync_api_request(rpcenv
, api_method
, formatter
, parts
, body
, uri_param
);
568 MethodDefinition
::Async(async_method
) => {
569 return handle_async_api_request(rpcenv
, async_method
, formatter
, parts
, body
, uri_param
);
574 // not Auth required for accessing files!
576 if method
!= hyper
::Method
::GET
{
577 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, format
!("Unsupported method"))));
581 let (ticket
, token
) = extract_auth_data(&parts
.headers
);
583 match check_auth(&method
, &ticket
, &token
) {
585 let new_token
= assemble_csrf_prevention_token(csrf_secret(), &username
);
586 return Box
::new(future
::ok(get_index(Some(username
), Some(new_token
))));
588 _
=> return delayed_response(get_index(None
, None
), delay_unauth_time
),
591 return Box
::new(future
::ok(get_index(None
, None
)));
594 let filename
= api
.find_alias(&components
);
595 return handle_static_file_download(filename
);
599 Box
::new(future
::err(http_err
!(NOT_FOUND
, "Path not found.".to_string())))