1 use std
::collections
::HashMap
;
2 use std
::hash
::BuildHasher
;
3 use std
::path
::{Path, PathBuf}
;
6 use std
::task
::{Context, Poll}
;
9 use futures
::future
::{self, Either, FutureExt, TryFutureExt}
;
10 use futures
::stream
::TryStreamExt
;
12 use hyper
::http
::request
::Parts
;
13 use hyper
::rt
::Future
;
14 use hyper
::{Body, Request, Response, StatusCode}
;
15 use serde_json
::{json, Value}
;
17 use url
::form_urlencoded
;
19 use proxmox
::api
::http_err
;
20 use proxmox
::api
::{ApiFuture, ApiHandler, ApiMethod, HttpError}
;
21 use proxmox
::api
::{RpcEnvironment, RpcEnvironmentType}
;
22 use proxmox
::api
::schema
::{parse_simple_value, verify_json_object, parse_parameter_strings}
;
24 use super::environment
::RestEnvironment
;
25 use super::formatter
::*;
28 use crate::auth_helpers
::*;
31 extern "C" { fn tzset(); }
33 pub struct RestServer
{
34 pub api_config
: Arc
<ApiConfig
>,
39 pub fn new(api_config
: ApiConfig
) -> Self {
40 Self { api_config: Arc::new(api_config) }
44 impl tower_service
::Service
<&tokio_openssl
::SslStream
<tokio
::net
::TcpStream
>> for RestServer
{
45 type Response
= ApiService
;
47 type Future
= Pin
<Box
<dyn Future
<Output
= Result
<ApiService
, Error
>> + Send
>>;
49 fn poll_ready(&mut self, _cx
: &mut Context
) -> Poll
<Result
<(), Self::Error
>> {
53 fn call(&mut self, ctx
: &tokio_openssl
::SslStream
<tokio
::net
::TcpStream
>) -> Self::Future
{
54 match ctx
.get_ref().peer_addr() {
56 future
::err(format_err
!("unable to get peer address - {}", err
)).boxed()
59 future
::ok(ApiService { peer, api_config: self.api_config.clone() }
).boxed()
65 impl tower_service
::Service
<&tokio
::net
::TcpStream
> for RestServer
{
66 type Response
= ApiService
;
68 type Future
= Pin
<Box
<dyn Future
<Output
= Result
<ApiService
, Error
>> + Send
>>;
70 fn poll_ready(&mut self, _cx
: &mut Context
) -> Poll
<Result
<(), Self::Error
>> {
74 fn call(&mut self, ctx
: &tokio
::net
::TcpStream
) -> Self::Future
{
75 match ctx
.peer_addr() {
77 future
::err(format_err
!("unable to get peer address - {}", err
)).boxed()
80 future
::ok(ApiService { peer, api_config: self.api_config.clone() }
).boxed()
86 pub struct ApiService
{
87 pub peer
: std
::net
::SocketAddr
,
88 pub api_config
: Arc
<ApiConfig
>,
92 peer
: &std
::net
::SocketAddr
,
93 method
: hyper
::Method
,
95 resp
: &Response
<Body
>,
98 if resp
.extensions().get
::<NoLogExtension
>().is_some() { return; }
;
100 let status
= resp
.status();
102 if !(status
.is_success() || status
.is_informational()) {
103 let reason
= status
.canonical_reason().unwrap_or("unknown reason");
105 let mut message
= "request failed";
106 if let Some(data
) = resp
.extensions().get
::<ErrorMessageExtension
>() {
110 log
::error
!("{} {}: {} {}: [client {}] {}", method
.as_str(), path
, status
.as_str(), reason
, peer
, message
);
114 impl tower_service
::Service
<Request
<Body
>> for ApiService
{
115 type Response
= Response
<Body
>;
117 type Future
= Pin
<Box
<dyn Future
<Output
= Result
<Response
<Body
>, Self::Error
>> + Send
>>;
119 fn poll_ready(&mut self, _cx
: &mut Context
) -> Poll
<Result
<(), Self::Error
>> {
123 fn call(&mut self, req
: Request
<Body
>) -> Self::Future
{
124 let path
= req
.uri().path().to_owned();
125 let method
= req
.method().clone();
127 let peer
= self.peer
;
128 Pin
::from(handle_request(self.api_config
.clone(), req
))
129 .map(move |result
| match result
{
131 log_response(&peer
, method
, &path
, &res
);
132 Ok
::<_
, Self::Error
>(res
)
135 if let Some(apierr
) = err
.downcast_ref
::<HttpError
>() {
136 let mut resp
= Response
::new(Body
::from(apierr
.message
.clone()));
137 *resp
.status_mut() = apierr
.code
;
138 log_response(&peer
, method
, &path
, &resp
);
141 let mut resp
= Response
::new(Body
::from(err
.to_string()));
142 *resp
.status_mut() = StatusCode
::BAD_REQUEST
;
143 log_response(&peer
, method
, &path
, &resp
);
152 fn get_request_parameters_async
<S
: '
static + BuildHasher
+ Send
>(
153 info
: &'
static ApiMethod
,
156 uri_param
: HashMap
<String
, String
, S
>,
157 ) -> Box
<dyn Future
<Output
= Result
<Value
, failure
::Error
>> + Send
>
159 let mut is_json
= false;
161 if let Some(value
) = parts
.headers
.get(header
::CONTENT_TYPE
) {
162 match value
.to_str().map(|v
| v
.split('
;'
).next()) {
163 Ok(Some("application/x-www-form-urlencoded")) => {
166 Ok(Some("application/json")) => {
170 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, "unsupported content type".to_string())));
176 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("Promlems reading request body: {}", err
)))
177 .try_fold(Vec
::new(), |mut acc
, chunk
| async
move {
178 if acc
.len() + chunk
.len() < 64*1024 { //fimxe: max request body size?
179 acc
.extend_from_slice(&*chunk
);
182 Err(http_err
!(BAD_REQUEST
, "Request body too large".to_string()))
185 .and_then(move |body
| async
move {
186 let utf8
= std
::str::from_utf8(&body
)?
;
188 let obj_schema
= &info
.parameters
;
191 let mut params
: Value
= serde_json
::from_str(utf8
)?
;
192 for (k
, v
) in uri_param
{
193 if let Some((_optional
, prop_schema
)) = obj_schema
.lookup(&k
) {
194 params
[&k
] = parse_simple_value(&v
, prop_schema
)?
;
197 verify_json_object(¶ms
, obj_schema
)?
;
201 let mut param_list
: Vec
<(String
, String
)> = vec
![];
203 if !utf8
.is_empty() {
204 for (k
, v
) in form_urlencoded
::parse(utf8
.as_bytes()).into_owned() {
205 param_list
.push((k
, v
));
209 if let Some(query_str
) = parts
.uri
.query() {
210 for (k
, v
) in form_urlencoded
::parse(query_str
.as_bytes()).into_owned() {
211 if k
== "_dc" { continue; }
// skip extjs "disable cache" parameter
212 param_list
.push((k
, v
));
216 for (k
, v
) in uri_param
{
217 param_list
.push((k
.clone(), v
.clone()));
220 let params
= parse_parameter_strings(¶m_list
, obj_schema
, true)?
;
228 struct NoLogExtension();
230 fn proxy_protected_request(
231 info
: &'
static ApiMethod
,
236 let mut uri_parts
= parts
.uri
.clone().into_parts();
238 uri_parts
.scheme
= Some(http
::uri
::Scheme
::HTTP
);
239 uri_parts
.authority
= Some(http
::uri
::Authority
::from_static("127.0.0.1:82"));
240 let new_uri
= http
::Uri
::from_parts(uri_parts
).unwrap();
244 let request
= Request
::from_parts(parts
, req_body
);
246 let resp
= hyper
::client
::Client
::new()
248 .map_err(Error
::from
)
250 resp
.extensions_mut().insert(NoLogExtension());
255 let reload_timezone
= info
.reload_timezone
;
256 Box
::new(async
move {
257 let result
= resp
.await
;
267 pub fn handle_sync_api_request
<Env
: RpcEnvironment
, S
: '
static + BuildHasher
+ Send
>(
269 info
: &'
static ApiMethod
,
270 formatter
: &'
static OutputFormatter
,
273 uri_param
: HashMap
<String
, String
, S
>,
276 let handler
= match info
.handler
{
277 ApiHandler
::Async(_
) => {
280 ApiHandler
::Sync(handler
) => handler
,
283 let params
= get_request_parameters_async(info
, parts
, req_body
, uri_param
);
285 let delay_unauth_time
= std
::time
::Instant
::now() + std
::time
::Duration
::from_millis(3000);
287 let resp
= Pin
::from(params
)
288 .and_then(move |params
| {
289 let mut delay
= false;
291 let resp
= match (handler
)(params
, info
, &mut rpcenv
) {
292 Ok(data
) => (formatter
.format_data
)(data
, &rpcenv
),
294 if let Some(httperr
) = err
.downcast_ref
::<HttpError
>() {
295 if httperr
.code
== StatusCode
::UNAUTHORIZED
{
299 (formatter
.format_error
)(err
)
303 if info
.reload_timezone
{
308 Either
::Left(delayed_response(resp
, delay_unauth_time
))
310 Either
::Right(future
::ok(resp
))
313 .or_else(move |err
| {
314 future
::ok((formatter
.format_error
)(err
))
320 pub fn handle_async_api_request
<Env
: RpcEnvironment
>(
322 info
: &'
static ApiMethod
,
323 formatter
: &'
static OutputFormatter
,
326 uri_param
: HashMap
<String
, String
>,
329 let handler
= match info
.handler
{
330 ApiHandler
::Sync(_
) => {
333 ApiHandler
::Async(handler
) => handler
,
336 // fixme: convert parameters to Json
337 let mut param_list
: Vec
<(String
, String
)> = vec
![];
339 if let Some(query_str
) = parts
.uri
.query() {
340 for (k
, v
) in form_urlencoded
::parse(query_str
.as_bytes()).into_owned() {
341 if k
== "_dc" { continue; }
// skip extjs "disable cache" parameter
342 param_list
.push((k
, v
));
346 for (k
, v
) in uri_param
{
347 param_list
.push((k
.clone(), v
.clone()));
350 let params
= match parse_parameter_strings(¶m_list
, &info
.parameters
, true) {
353 let resp
= (formatter
.format_error
)(Error
::from(err
));
354 return Box
::new(future
::ok(resp
));
358 match (handler
)(parts
, req_body
, params
, info
, Box
::new(rpcenv
)) {
359 Ok(future
) => future
,
361 let resp
= (formatter
.format_error
)(err
);
362 Box
::new(future
::ok(resp
))
367 fn get_index(username
: Option
<String
>, token
: Option
<String
>) -> Response
<Body
> {
369 let nodename
= proxmox
::tools
::nodename();
370 let username
= username
.unwrap_or_else(|| String
::from(""));
372 let token
= token
.unwrap_or_else(|| String
::from(""));
375 "Setup": { "auth_cookie_name": "PBSAuthCookie" }
,
376 "NodeName": nodename
,
377 "UserName": username
,
378 "CSRFPreventionToken": token
,
381 let index
= format
!(r
###"
385 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
386 <meta http-equiv="X-UA-Compatible" content="IE=edge">
387 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
388 <title>Proxmox Backup Server</title>
389 <link rel="icon" sizes="128x128" href="/images/logo-128.png" />
390 <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
391 <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
392 <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
393 <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
394 <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
395 <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
396 <script type="text/javascript" src="/extjs/charts-debug.js"></script>
397 <script type="text/javascript">
400 <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script>
401 <script type="text/javascript" src="/extjs/locale/locale-en.js"></script>
402 <script type="text/javascript">
403 Ext.History.fieldid = 'x-history-field';
405 <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script>
408 <!-- Fields required for history management -->
409 <form id="history-form" class="x-hidden">
410 <input type="hidden" id="x-history-field"/>
414 "###, setup.to_string());
417 .status(StatusCode
::OK
)
418 .header(header
::CONTENT_TYPE
, "text/html")
423 fn extension_to_content_type(filename
: &Path
) -> (&'
static str, bool
) {
425 if let Some(ext
) = filename
.extension().and_then(|osstr
| osstr
.to_str()) {
427 "css" => ("text/css", false),
428 "html" => ("text/html", false),
429 "js" => ("application/javascript", false),
430 "json" => ("application/json", false),
431 "map" => ("application/json", false),
432 "png" => ("image/png", true),
433 "ico" => ("image/x-icon", true),
434 "gif" => ("image/gif", true),
435 "svg" => ("image/svg+xml", false),
436 "jar" => ("application/java-archive", true),
437 "woff" => ("application/font-woff", true),
438 "woff2" => ("application/font-woff2", true),
439 "ttf" => ("application/font-snft", true),
440 "pdf" => ("application/pdf", true),
441 "epub" => ("application/epub+zip", true),
442 "mp3" => ("audio/mpeg", true),
443 "oga" => ("audio/ogg", true),
444 "tgz" => ("application/x-compressed-tar", true),
445 _
=> ("application/octet-stream", false),
449 ("application/octet-stream", false)
452 async
fn simple_static_file_download(filename
: PathBuf
) -> Result
<Response
<Body
>, Error
> {
454 let (content_type
, _nocomp
) = extension_to_content_type(&filename
);
456 use tokio
::io
::AsyncReadExt
;
458 let mut file
= File
::open(filename
)
460 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File open failed: {}", err
)))?
;
462 let mut data
: Vec
<u8> = Vec
::new();
463 file
.read_to_end(&mut data
)
465 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File read failed: {}", err
)))?
;
467 let mut response
= Response
::new(data
.into());
468 response
.headers_mut().insert(
469 header
::CONTENT_TYPE
,
470 header
::HeaderValue
::from_static(content_type
));
474 async
fn chuncked_static_file_download(filename
: PathBuf
) -> Result
<Response
<Body
>, Error
> {
475 let (content_type
, _nocomp
) = extension_to_content_type(&filename
);
477 let file
= File
::open(filename
)
479 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File open failed: {}", err
)))?
;
481 let payload
= tokio
::codec
::FramedRead
::new(file
, tokio
::codec
::BytesCodec
::new())
482 .map_ok(|bytes
| hyper
::Chunk
::from(bytes
.freeze()));
483 let body
= Body
::wrap_stream(payload
);
485 // fixme: set other headers ?
486 Ok(Response
::builder()
487 .status(StatusCode
::OK
)
488 .header(header
::CONTENT_TYPE
, content_type
)
494 fn handle_static_file_download(filename
: PathBuf
) -> ApiFuture
{
496 let response
= tokio
::fs
::metadata(filename
.clone())
497 .map_err(|err
| http_err
!(BAD_REQUEST
, format
!("File access problems: {}", err
)))
498 .and_then(|metadata
| async
move {
499 if metadata
.len() < 1024*32 {
500 simple_static_file_download(filename
).await
502 chuncked_static_file_download(filename
).await
509 fn extract_auth_data(headers
: &http
::HeaderMap
) -> (Option
<String
>, Option
<String
>) {
511 let mut ticket
= None
;
512 if let Some(raw_cookie
) = headers
.get("COOKIE") {
513 if let Ok(cookie
) = raw_cookie
.to_str() {
514 ticket
= tools
::extract_auth_cookie(cookie
, "PBSAuthCookie");
518 let token
= match headers
.get("CSRFPreventionToken").map(|v
| v
.to_str()) {
519 Some(Ok(v
)) => Some(v
.to_owned()),
526 fn check_auth(method
: &hyper
::Method
, ticket
: &Option
<String
>, token
: &Option
<String
>) -> Result
<String
, Error
> {
528 let ticket_lifetime
= tools
::ticket
::TICKET_LIFETIME
;
530 let username
= match ticket
{
531 Some(ticket
) => match tools
::ticket
::verify_rsa_ticket(public_auth_key(), "PBS", &ticket
, None
, -300, ticket_lifetime
) {
532 Ok((_age
, Some(username
))) => username
.to_owned(),
533 Ok((_
, None
)) => bail
!("ticket without username."),
534 Err(err
) => return Err(err
),
536 None
=> bail
!("missing ticket"),
539 if method
!= hyper
::Method
::GET
{
540 if let Some(token
) = token
{
541 println
!("CSRF prevention token: {:?}", token
);
542 verify_csrf_prevention_token(csrf_secret(), &username
, &token
, -300, ticket_lifetime
)?
;
544 bail
!("missing CSRF prevention token");
551 async
fn delayed_response(
552 resp
: Response
<Body
>,
553 delay_unauth_time
: std
::time
::Instant
,
554 ) -> Result
<Response
<Body
>, Error
> {
555 tokio
::timer
::delay(delay_unauth_time
).await
;
559 pub fn handle_request(api
: Arc
<ApiConfig
>, req
: Request
<Body
>) -> ApiFuture
{
561 let (parts
, body
) = req
.into_parts();
563 let method
= parts
.method
.clone();
565 let (path
, components
) = match tools
::normalize_uri_path(parts
.uri
.path()) {
567 Err(err
) => return Box
::new(future
::err(http_err
!(BAD_REQUEST
, err
.to_string()))),
570 let comp_len
= components
.len();
572 println
!("REQUEST {} {}", method
, path
);
573 println
!("COMPO {:?}", components
);
575 let env_type
= api
.env_type();
576 let mut rpcenv
= RestEnvironment
::new(env_type
);
578 let delay_unauth_time
= std
::time
::Instant
::now() + std
::time
::Duration
::from_millis(3000);
580 if comp_len
>= 1 && components
[0] == "api2" {
583 let format
= components
[1];
584 let formatter
= match format
{
585 "json" => &JSON_FORMATTER
,
586 "extjs" => &EXTJS_FORMATTER
,
588 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, format
!("Unsupported output format '{}'.", format
))));
592 let mut uri_param
= HashMap
::new();
594 if comp_len
== 4 && components
[2] == "access" && components
[3] == "ticket" {
595 // explicitly allow those calls without auth
597 let (ticket
, token
) = extract_auth_data(&parts
.headers
);
598 match check_auth(&method
, &ticket
, &token
) {
601 // fixme: check permissions
603 rpcenv
.set_user(Some(username
));
606 // always delay unauthorized calls by 3 seconds (from start of request)
607 let err
= http_err
!(UNAUTHORIZED
, format
!("permission check failed - {}", err
));
609 delayed_response((formatter
.format_error
)(err
), delay_unauth_time
)
615 match api
.find_method(&components
[2..], method
, &mut uri_param
) {
617 let err
= http_err
!(NOT_FOUND
, "Path not found.".to_string());
618 return Box
::new(future
::ok((formatter
.format_error
)(err
)));
620 Some(api_method
) => {
621 if api_method
.protected
&& env_type
== RpcEnvironmentType
::PUBLIC
{
622 return proxy_protected_request(api_method
, parts
, body
);
624 match api_method
.handler
{
625 ApiHandler
::Sync(_
) => {
626 return handle_sync_api_request(rpcenv
, api_method
, formatter
, parts
, body
, uri_param
);
628 ApiHandler
::Async(_
) => {
629 return handle_async_api_request(rpcenv
, api_method
, formatter
, parts
, body
, uri_param
);
637 // not Auth required for accessing files!
639 if method
!= hyper
::Method
::GET
{
640 return Box
::new(future
::err(http_err
!(BAD_REQUEST
, "Unsupported method".to_string())));
644 let (ticket
, token
) = extract_auth_data(&parts
.headers
);
646 match check_auth(&method
, &ticket
, &token
) {
648 let new_token
= assemble_csrf_prevention_token(csrf_secret(), &username
);
649 return Box
::new(future
::ok(get_index(Some(username
), Some(new_token
))));
652 return Box
::new(delayed_response(get_index(None
, None
), delay_unauth_time
));
656 return Box
::new(future
::ok(get_index(None
, None
)));
659 let filename
= api
.find_alias(&components
);
660 return handle_static_file_download(filename
);
664 Box
::new(future
::err(http_err
!(NOT_FOUND
, "Path not found.".to_string())))