]>
Commit | Line | Data |
---|---|---|
91e45873 | 1 | use std::collections::HashMap; |
826bb982 | 2 | use std::path::{Path, PathBuf}; |
91e45873 | 3 | use std::pin::Pin; |
9bc17e8d | 4 | use std::sync::Arc; |
91e45873 | 5 | use std::task::{Context, Poll}; |
9bc17e8d DM |
6 | |
7 | use failure::*; | |
91e45873 WB |
8 | use futures::future::{self, Either, FutureExt, TryFutureExt}; |
9 | use futures::stream::TryStreamExt; | |
10 | use hyper::header; | |
11 | use hyper::http::request::Parts; | |
12 | use hyper::rt::Future; | |
13 | use hyper::{Body, Request, Response, StatusCode}; | |
f4c514c1 | 14 | use serde_json::{json, Value}; |
9bc17e8d | 15 | use tokio::fs::File; |
91e45873 | 16 | use url::form_urlencoded; |
9bc17e8d | 17 | |
91e45873 WB |
18 | use super::environment::RestEnvironment; |
19 | use super::formatter::*; | |
20 | use crate::api_schema::config::*; | |
21 | use crate::api_schema::router::*; | |
22 | use crate::api_schema::*; | |
23 | use crate::auth_helpers::*; | |
24 | use crate::tools; | |
9bc17e8d | 25 | |
4b2cdeb9 DM |
26 | extern "C" { fn tzset(); } |
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 | ||
91e45873 WB |
39 | impl tower_service::Service<&tokio_openssl::SslStream<tokio::net::TcpStream>> for RestServer { |
40 | type Response = ApiService; | |
7fb4f564 | 41 | type Error = Error; |
91e45873 WB |
42 | type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>; |
43 | ||
44 | fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> { | |
45 | Poll::Ready(Ok(())) | |
46 | } | |
47 | ||
48 | fn call(&mut self, ctx: &tokio_openssl::SslStream<tokio::net::TcpStream>) -> Self::Future { | |
49 | match ctx.get_ref().peer_addr() { | |
80af0467 | 50 | Err(err) => { |
91e45873 | 51 | future::err(format_err!("unable to get peer address - {}", err)).boxed() |
80af0467 DM |
52 | } |
53 | Ok(peer) => { | |
91e45873 | 54 | future::ok(ApiService { peer, api_config: self.api_config.clone() }).boxed() |
80af0467 DM |
55 | } |
56 | } | |
f0b10921 DM |
57 | } |
58 | } | |
59 | ||
91e45873 WB |
60 | impl tower_service::Service<&tokio::net::TcpStream> for RestServer { |
61 | type Response = ApiService; | |
7fb4f564 | 62 | type Error = Error; |
91e45873 WB |
63 | type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>; |
64 | ||
65 | fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> { | |
66 | Poll::Ready(Ok(())) | |
67 | } | |
68 | ||
69 | fn call(&mut self, ctx: &tokio::net::TcpStream) -> Self::Future { | |
80af0467 DM |
70 | match ctx.peer_addr() { |
71 | Err(err) => { | |
91e45873 | 72 | future::err(format_err!("unable to get peer address - {}", err)).boxed() |
80af0467 DM |
73 | } |
74 | Ok(peer) => { | |
91e45873 | 75 | future::ok(ApiService { peer, api_config: self.api_config.clone() }).boxed() |
80af0467 DM |
76 | } |
77 | } | |
7fb4f564 DM |
78 | } |
79 | } | |
80 | ||
f0b10921 | 81 | pub struct ApiService { |
7fb4f564 | 82 | pub peer: std::net::SocketAddr, |
f0b10921 DM |
83 | pub api_config: Arc<ApiConfig>, |
84 | } | |
85 | ||
7fb4f564 DM |
86 | fn log_response( |
87 | peer: &std::net::SocketAddr, | |
88 | method: hyper::Method, | |
89 | path: &str, | |
90 | resp: &Response<Body>, | |
91 | ) { | |
7e03988c | 92 | |
d4736445 | 93 | if resp.extensions().get::<NoLogExtension>().is_some() { return; }; |
7e03988c | 94 | |
d4736445 | 95 | let status = resp.status(); |
7e03988c | 96 | |
1133fe9a | 97 | if !(status.is_success() || status.is_informational()) { |
d4736445 | 98 | let reason = status.canonical_reason().unwrap_or("unknown reason"); |
44c00c0d | 99 | |
d4736445 DM |
100 | let mut message = "request failed"; |
101 | if let Some(data) = resp.extensions().get::<ErrorMessageExtension>() { | |
102 | message = &data.0; | |
78a1fa67 | 103 | } |
d4736445 | 104 | |
7fb4f564 | 105 | log::error!("{} {}: {} {}: [client {}] {}", method.as_str(), path, status.as_str(), reason, peer, message); |
78a1fa67 DM |
106 | } |
107 | } | |
f0b10921 | 108 | |
91e45873 WB |
109 | impl tower_service::Service<Request<Body>> for ApiService { |
110 | type Response = Response<Body>; | |
7fb4f564 | 111 | type Error = Error; |
91e45873 WB |
112 | type Future = Pin<Box<dyn Future<Output = Result<Response<Body>, Self::Error>> + Send>>; |
113 | ||
114 | fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> { | |
115 | Poll::Ready(Ok(())) | |
116 | } | |
f0b10921 | 117 | |
91e45873 | 118 | fn call(&mut self, req: Request<Body>) -> Self::Future { |
78a1fa67 | 119 | let path = req.uri().path().to_owned(); |
d4736445 DM |
120 | let method = req.method().clone(); |
121 | ||
7fb4f564 | 122 | let peer = self.peer.clone(); |
91e45873 WB |
123 | Pin::from(handle_request(self.api_config.clone(), req)) |
124 | .map(move |result| match result { | |
78a1fa67 | 125 | Ok(res) => { |
7fb4f564 DM |
126 | log_response(&peer, method, &path, &res); |
127 | Ok::<_, Self::Error>(res) | |
78a1fa67 | 128 | } |
f0b10921 | 129 | Err(err) => { |
0dffe3f9 | 130 | if let Some(apierr) = err.downcast_ref::<HttpError>() { |
f0b10921 DM |
131 | let mut resp = Response::new(Body::from(apierr.message.clone())); |
132 | *resp.status_mut() = apierr.code; | |
7fb4f564 | 133 | log_response(&peer, method, &path, &resp); |
f0b10921 DM |
134 | Ok(resp) |
135 | } else { | |
136 | let mut resp = Response::new(Body::from(err.to_string())); | |
137 | *resp.status_mut() = StatusCode::BAD_REQUEST; | |
7fb4f564 | 138 | log_response(&peer, method, &path, &resp); |
f0b10921 DM |
139 | Ok(resp) |
140 | } | |
141 | } | |
91e45873 WB |
142 | }) |
143 | .boxed() | |
f0b10921 DM |
144 | } |
145 | } | |
146 | ||
279ecfdf DM |
147 | fn get_request_parameters_async( |
148 | info: &'static ApiMethod, | |
9bc17e8d DM |
149 | parts: Parts, |
150 | req_body: Body, | |
e7ea17de | 151 | uri_param: HashMap<String, String>, |
91e45873 | 152 | ) -> Box<dyn Future<Output = Result<Value, failure::Error>> + Send> |
9bc17e8d | 153 | { |
0ffbccce DM |
154 | let mut is_json = false; |
155 | ||
156 | if let Some(value) = parts.headers.get(header::CONTENT_TYPE) { | |
8346f0d5 DM |
157 | match value.to_str().map(|v| v.split(';').next()) { |
158 | Ok(Some("application/x-www-form-urlencoded")) => { | |
159 | is_json = false; | |
160 | } | |
161 | Ok(Some("application/json")) => { | |
162 | is_json = true; | |
163 | } | |
164 | _ => { | |
165 | return Box::new(future::err(http_err!(BAD_REQUEST, format!("unsupported content type")))); | |
166 | } | |
0ffbccce DM |
167 | } |
168 | } | |
169 | ||
9bc17e8d | 170 | let resp = req_body |
4dcf43d2 | 171 | .map_err(|err| http_err!(BAD_REQUEST, format!("Promlems reading request body: {}", err))) |
91e45873 | 172 | .try_fold(Vec::new(), |mut acc, chunk| async move { |
9bc17e8d DM |
173 | if acc.len() + chunk.len() < 64*1024 { //fimxe: max request body size? |
174 | acc.extend_from_slice(&*chunk); | |
175 | Ok(acc) | |
91e45873 WB |
176 | } else { |
177 | Err(http_err!(BAD_REQUEST, format!("Request body too large"))) | |
9bc17e8d | 178 | } |
9bc17e8d | 179 | }) |
91e45873 | 180 | .and_then(move |body| async move { |
1ed86a0b | 181 | let utf8 = std::str::from_utf8(&body)?; |
9bc17e8d | 182 | |
0ffbccce DM |
183 | let obj_schema = &info.parameters; |
184 | ||
185 | if is_json { | |
186 | let mut params: Value = serde_json::from_str(utf8)?; | |
187 | for (k, v) in uri_param { | |
188 | if let Some((_optional, prop_schema)) = obj_schema.properties.get::<str>(&k) { | |
189 | params[&k] = parse_simple_value(&v, prop_schema)?; | |
190 | } | |
191 | } | |
34114e26 | 192 | verify_json_object(¶ms, obj_schema)?; |
0ffbccce DM |
193 | return Ok(params); |
194 | } | |
195 | ||
e7ea17de | 196 | let mut param_list: Vec<(String, String)> = vec![]; |
9bc17e8d | 197 | |
1ed86a0b WB |
198 | if utf8.len() > 0 { |
199 | for (k, v) in form_urlencoded::parse(utf8.as_bytes()).into_owned() { | |
e7ea17de DM |
200 | param_list.push((k, v)); |
201 | } | |
202 | ||
9bc17e8d DM |
203 | } |
204 | ||
205 | if let Some(query_str) = parts.uri.query() { | |
e7ea17de | 206 | for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() { |
1571873d | 207 | if k == "_dc" { continue; } // skip extjs "disable cache" parameter |
e7ea17de | 208 | param_list.push((k, v)); |
9bc17e8d DM |
209 | } |
210 | } | |
211 | ||
e7ea17de DM |
212 | for (k, v) in uri_param { |
213 | param_list.push((k.clone(), v.clone())); | |
214 | } | |
215 | ||
0ffbccce | 216 | let params = parse_parameter_strings(¶m_list, obj_schema, true)?; |
e7ea17de | 217 | |
9bc17e8d | 218 | Ok(params) |
91e45873 | 219 | }.boxed()); |
9bc17e8d DM |
220 | |
221 | Box::new(resp) | |
222 | } | |
223 | ||
7171b3e0 DM |
224 | struct NoLogExtension(); |
225 | ||
c8f3f9b1 | 226 | fn proxy_protected_request( |
4b2cdeb9 | 227 | info: &'static ApiMethod, |
a3da38dd | 228 | mut parts: Parts, |
f1204833 | 229 | req_body: Body, |
91e45873 | 230 | ) -> BoxFut { |
f1204833 | 231 | |
a3da38dd DM |
232 | let mut uri_parts = parts.uri.clone().into_parts(); |
233 | ||
234 | uri_parts.scheme = Some(http::uri::Scheme::HTTP); | |
235 | uri_parts.authority = Some(http::uri::Authority::from_static("127.0.0.1:82")); | |
236 | let new_uri = http::Uri::from_parts(uri_parts).unwrap(); | |
237 | ||
238 | parts.uri = new_uri; | |
239 | ||
240 | let request = Request::from_parts(parts, req_body); | |
241 | ||
242 | let resp = hyper::client::Client::new() | |
243 | .request(request) | |
fc7f0352 | 244 | .map_err(Error::from) |
91e45873 | 245 | .map_ok(|mut resp| { |
1cb99c23 | 246 | resp.extensions_mut().insert(NoLogExtension()); |
7e03988c DM |
247 | resp |
248 | }); | |
a3da38dd | 249 | |
1cb99c23 | 250 | |
91e45873 WB |
251 | let reload_timezone = info.reload_timezone; |
252 | Box::new(async move { | |
253 | let result = resp.await; | |
254 | if reload_timezone { | |
255 | unsafe { | |
256 | tzset(); | |
257 | } | |
258 | } | |
259 | result | |
260 | }) | |
f1204833 DM |
261 | } |
262 | ||
f757b30e DM |
263 | pub fn handle_sync_api_request<Env: RpcEnvironment>( |
264 | mut rpcenv: Env, | |
279ecfdf | 265 | info: &'static ApiMethod, |
1571873d | 266 | formatter: &'static OutputFormatter, |
9bc17e8d DM |
267 | parts: Parts, |
268 | req_body: Body, | |
e7ea17de | 269 | uri_param: HashMap<String, String>, |
279ecfdf | 270 | ) -> BoxFut |
9bc17e8d | 271 | { |
e7ea17de | 272 | let params = get_request_parameters_async(info, parts, req_body, uri_param); |
9bc17e8d | 273 | |
a154a8e8 DM |
274 | let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); |
275 | ||
91e45873 | 276 | let resp = Pin::from(params) |
9bc17e8d | 277 | .and_then(move |params| { |
a154a8e8 | 278 | let mut delay = false; |
91e45873 | 279 | let resp = match (info.handler.as_ref().unwrap())(params, info, &mut rpcenv) { |
d55037e4 | 280 | Ok(data) => (formatter.format_data)(data, &rpcenv), |
a154a8e8 | 281 | Err(err) => { |
91e45873 WB |
282 | if let Some(httperr) = err.downcast_ref::<HttpError>() { |
283 | if httperr.code == StatusCode::UNAUTHORIZED { | |
284 | delay = true; | |
285 | } | |
a154a8e8 DM |
286 | } |
287 | (formatter.format_error)(err) | |
288 | } | |
6049b71f | 289 | }; |
a154a8e8 | 290 | |
4b2cdeb9 DM |
291 | if info.reload_timezone { |
292 | unsafe { tzset() }; | |
293 | } | |
294 | ||
a154a8e8 | 295 | if delay { |
91e45873 | 296 | Either::Left(delayed_response(resp, delay_unauth_time)) |
a154a8e8 | 297 | } else { |
91e45873 | 298 | Either::Right(future::ok(resp)) |
a154a8e8 | 299 | } |
1179e158 DM |
300 | }) |
301 | .or_else(move |err| { | |
91e45873 | 302 | future::ok((formatter.format_error)(err)) |
9bc17e8d DM |
303 | }); |
304 | ||
305 | Box::new(resp) | |
306 | } | |
307 | ||
f757b30e | 308 | pub fn handle_async_api_request<Env: RpcEnvironment>( |
b4b63e52 | 309 | rpcenv: Env, |
50cfb695 | 310 | info: &'static ApiAsyncMethod, |
cf16af2a DM |
311 | formatter: &'static OutputFormatter, |
312 | parts: Parts, | |
7e21da6e DM |
313 | req_body: Body, |
314 | uri_param: HashMap<String, String>, | |
315 | ) -> BoxFut | |
316 | { | |
317 | // fixme: convert parameters to Json | |
318 | let mut param_list: Vec<(String, String)> = vec![]; | |
319 | ||
cf16af2a DM |
320 | if let Some(query_str) = parts.uri.query() { |
321 | for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() { | |
322 | if k == "_dc" { continue; } // skip extjs "disable cache" parameter | |
323 | param_list.push((k, v)); | |
324 | } | |
325 | } | |
326 | ||
7e21da6e DM |
327 | for (k, v) in uri_param { |
328 | param_list.push((k.clone(), v.clone())); | |
329 | } | |
330 | ||
331 | let params = match parse_parameter_strings(¶m_list, &info.parameters, true) { | |
332 | Ok(v) => v, | |
333 | Err(err) => { | |
6049b71f | 334 | let resp = (formatter.format_error)(Error::from(err)); |
cf16af2a | 335 | return Box::new(future::ok(resp)); |
7e21da6e DM |
336 | } |
337 | }; | |
338 | ||
b4b63e52 | 339 | match (info.handler)(parts, req_body, params, info, Box::new(rpcenv)) { |
0ee0ad5b DM |
340 | Ok(future) => future, |
341 | Err(err) => { | |
6049b71f | 342 | let resp = (formatter.format_error)(Error::from(err)); |
0ee0ad5b DM |
343 | Box::new(future::ok(resp)) |
344 | } | |
345 | } | |
7e21da6e DM |
346 | } |
347 | ||
7f168523 | 348 | fn get_index(username: Option<String>, token: Option<String>) -> Response<Body> { |
f4c514c1 | 349 | |
f69adc81 | 350 | let nodename = proxmox::tools::nodename(); |
7f168523 DM |
351 | let username = username.unwrap_or(String::from("")); |
352 | ||
353 | let token = token.unwrap_or(String::from("")); | |
f4c514c1 DM |
354 | |
355 | let setup = json!({ | |
356 | "Setup": { "auth_cookie_name": "PBSAuthCookie" }, | |
357 | "NodeName": nodename, | |
358 | "UserName": username, | |
d15009c0 | 359 | "CSRFPreventionToken": token, |
f4c514c1 DM |
360 | }); |
361 | ||
362 | let index = format!(r###" | |
363 | <!DOCTYPE html> | |
364 | <html> | |
365 | <head> | |
366 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
367 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
368 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | |
369 | <title>Proxmox Backup Server</title> | |
8adbdb0a | 370 | <link rel="icon" sizes="128x128" href="/images/logo-128.png" /> |
f4c514c1 | 371 | <link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" /> |
8adbdb0a DM |
372 | <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" /> |
373 | <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" /> | |
f4c514c1 DM |
374 | <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" /> |
375 | <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script> | |
8adbdb0a DM |
376 | <script type="text/javascript" src="/extjs/ext-all-debug.js"></script> |
377 | <script type="text/javascript" src="/extjs/charts-debug.js"></script> | |
f4c514c1 DM |
378 | <script type="text/javascript"> |
379 | Proxmox = {}; | |
380 | </script> | |
8adbdb0a DM |
381 | <script type="text/javascript" src="/widgettoolkit/proxmoxlib.js"></script> |
382 | <script type="text/javascript" src="/extjs/locale/locale-en.js"></script> | |
f4c514c1 DM |
383 | <script type="text/javascript"> |
384 | Ext.History.fieldid = 'x-history-field'; | |
385 | </script> | |
5c7a1b15 | 386 | <script type="text/javascript" src="/js/proxmox-backup-gui.js"></script> |
f4c514c1 DM |
387 | </head> |
388 | <body> | |
389 | <!-- Fields required for history management --> | |
390 | <form id="history-form" class="x-hidden"> | |
391 | <input type="hidden" id="x-history-field"/> | |
392 | </form> | |
393 | </body> | |
394 | </html> | |
395 | "###, setup.to_string()); | |
396 | ||
7f168523 | 397 | Response::builder() |
d15009c0 DM |
398 | .status(StatusCode::OK) |
399 | .header(header::CONTENT_TYPE, "text/html") | |
d15009c0 | 400 | .body(index.into()) |
7f168523 | 401 | .unwrap() |
f4c514c1 DM |
402 | } |
403 | ||
826bb982 DM |
404 | fn extension_to_content_type(filename: &Path) -> (&'static str, bool) { |
405 | ||
406 | if let Some(ext) = filename.extension().and_then(|osstr| osstr.to_str()) { | |
407 | return match ext { | |
408 | "css" => ("text/css", false), | |
409 | "html" => ("text/html", false), | |
410 | "js" => ("application/javascript", false), | |
411 | "json" => ("application/json", false), | |
412 | "map" => ("application/json", false), | |
413 | "png" => ("image/png", true), | |
414 | "ico" => ("image/x-icon", true), | |
415 | "gif" => ("image/gif", true), | |
416 | "svg" => ("image/svg+xml", false), | |
417 | "jar" => ("application/java-archive", true), | |
418 | "woff" => ("application/font-woff", true), | |
419 | "woff2" => ("application/font-woff2", true), | |
420 | "ttf" => ("application/font-snft", true), | |
421 | "pdf" => ("application/pdf", true), | |
422 | "epub" => ("application/epub+zip", true), | |
423 | "mp3" => ("audio/mpeg", true), | |
424 | "oga" => ("audio/ogg", true), | |
425 | "tgz" => ("application/x-compressed-tar", true), | |
426 | _ => ("application/octet-stream", false), | |
427 | }; | |
428 | } | |
429 | ||
430 | ("application/octet-stream", false) | |
431 | } | |
432 | ||
91e45873 | 433 | async fn simple_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> { |
9bc17e8d | 434 | |
826bb982 DM |
435 | let (content_type, _nocomp) = extension_to_content_type(&filename); |
436 | ||
91e45873 | 437 | use tokio::io::AsyncReadExt; |
9bc17e8d | 438 | |
91e45873 WB |
439 | let mut file = File::open(filename) |
440 | .await | |
441 | .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))?; | |
9bc17e8d | 442 | |
91e45873 WB |
443 | let mut data: Vec<u8> = Vec::new(); |
444 | file.read_to_end(&mut data) | |
445 | .await | |
446 | .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err)))?; | |
447 | ||
448 | let mut response = Response::new(data.into()); | |
449 | response.headers_mut().insert( | |
450 | header::CONTENT_TYPE, | |
451 | header::HeaderValue::from_static(content_type)); | |
452 | Ok(response) | |
453 | } | |
454 | ||
455 | async fn chuncked_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> { | |
826bb982 DM |
456 | let (content_type, _nocomp) = extension_to_content_type(&filename); |
457 | ||
91e45873 WB |
458 | let file = File::open(filename) |
459 | .await | |
460 | .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))?; | |
461 | ||
462 | let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()) | |
463 | .map_ok(|bytes| hyper::Chunk::from(bytes.freeze())); | |
464 | let body = Body::wrap_stream(payload); | |
465 | ||
466 | // fixme: set other headers ? | |
467 | Ok(Response::builder() | |
468 | .status(StatusCode::OK) | |
469 | .header(header::CONTENT_TYPE, content_type) | |
470 | .body(body) | |
471 | .unwrap() | |
472 | ) | |
9bc17e8d DM |
473 | } |
474 | ||
475 | fn handle_static_file_download(filename: PathBuf) -> BoxFut { | |
476 | ||
477 | let response = tokio::fs::metadata(filename.clone()) | |
4dcf43d2 | 478 | .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err))) |
91e45873 | 479 | .and_then(|metadata| async move { |
9bc17e8d | 480 | if metadata.len() < 1024*32 { |
91e45873 | 481 | simple_static_file_download(filename).await |
9bc17e8d | 482 | } else { |
91e45873 WB |
483 | chuncked_static_file_download(filename).await |
484 | } | |
9bc17e8d DM |
485 | }); |
486 | ||
487 | return Box::new(response); | |
488 | } | |
489 | ||
5ddf8cb1 DM |
490 | fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>) { |
491 | ||
492 | let mut ticket = None; | |
493 | if let Some(raw_cookie) = headers.get("COOKIE") { | |
494 | if let Ok(cookie) = raw_cookie.to_str() { | |
495 | ticket = tools::extract_auth_cookie(cookie, "PBSAuthCookie"); | |
496 | } | |
497 | } | |
498 | ||
499 | let token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) { | |
500 | Some(Ok(v)) => Some(v.to_owned()), | |
501 | _ => None, | |
502 | }; | |
503 | ||
504 | (ticket, token) | |
505 | } | |
506 | ||
7f168523 | 507 | fn check_auth(method: &hyper::Method, ticket: &Option<String>, token: &Option<String>) -> Result<String, Error> { |
5ddf8cb1 | 508 | |
e5662b04 | 509 | let ticket_lifetime = tools::ticket::TICKET_LIFETIME; |
5ddf8cb1 DM |
510 | |
511 | let username = match ticket { | |
512 | Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) { | |
513 | Ok((_age, Some(username))) => username.to_owned(), | |
514 | Ok((_, None)) => bail!("ticket without username."), | |
515 | Err(err) => return Err(err), | |
516 | } | |
517 | None => bail!("missing ticket"), | |
518 | }; | |
519 | ||
520 | if method != hyper::Method::GET { | |
521 | if let Some(token) = token { | |
8225aa2f | 522 | println!("CSRF prevention token: {:?}", token); |
5ddf8cb1 DM |
523 | verify_csrf_prevention_token(csrf_secret(), &username, &token, -300, ticket_lifetime)?; |
524 | } else { | |
8225aa2f | 525 | bail!("missing CSRF prevention token"); |
5ddf8cb1 DM |
526 | } |
527 | } | |
528 | ||
529 | Ok(username) | |
530 | } | |
531 | ||
91e45873 WB |
532 | async fn delayed_response( |
533 | resp: Response<Body>, | |
534 | delay_unauth_time: std::time::Instant, | |
535 | ) -> Result<Response<Body>, Error> { | |
536 | tokio::timer::Delay::new(delay_unauth_time) | |
537 | .await; | |
538 | Ok(resp) | |
7f168523 DM |
539 | } |
540 | ||
141de837 DM |
541 | pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut { |
542 | ||
543 | let (parts, body) = req.into_parts(); | |
544 | ||
545 | let method = parts.method.clone(); | |
546 | ||
3578d99f | 547 | let (path, components) = match tools::normalize_uri_path(parts.uri.path()) { |
141de837 DM |
548 | Ok((p,c)) => (p, c), |
549 | Err(err) => return Box::new(future::err(http_err!(BAD_REQUEST, err.to_string()))), | |
550 | }; | |
551 | ||
9bc17e8d DM |
552 | let comp_len = components.len(); |
553 | ||
554 | println!("REQUEST {} {}", method, path); | |
555 | println!("COMPO {:?}", components); | |
556 | ||
f1204833 DM |
557 | let env_type = api.env_type(); |
558 | let mut rpcenv = RestEnvironment::new(env_type); | |
e82dad97 | 559 | |
b9903d63 DM |
560 | let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); |
561 | ||
576e3bf2 | 562 | if comp_len >= 1 && components[0] == "api2" { |
5ddf8cb1 | 563 | |
9bc17e8d DM |
564 | if comp_len >= 2 { |
565 | let format = components[1]; | |
1571873d DM |
566 | let formatter = match format { |
567 | "json" => &JSON_FORMATTER, | |
568 | "extjs" => &EXTJS_FORMATTER, | |
569 | _ => { | |
570 | return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format)))); | |
571 | } | |
572 | }; | |
9bc17e8d | 573 | |
e7ea17de DM |
574 | let mut uri_param = HashMap::new(); |
575 | ||
b9903d63 DM |
576 | if comp_len == 4 && components[2] == "access" && components[3] == "ticket" { |
577 | // explicitly allow those calls without auth | |
578 | } else { | |
5ddf8cb1 | 579 | let (ticket, token) = extract_auth_data(&parts.headers); |
7f168523 | 580 | match check_auth(&method, &ticket, &token) { |
5ddf8cb1 DM |
581 | Ok(username) => { |
582 | ||
583 | // fixme: check permissions | |
584 | ||
585 | rpcenv.set_user(Some(username)); | |
586 | } | |
587 | Err(err) => { | |
588 | // always delay unauthorized calls by 3 seconds (from start of request) | |
589 | let err = http_err!(UNAUTHORIZED, format!("permission check failed - {}", err)); | |
91e45873 WB |
590 | return Box::new( |
591 | delayed_response((formatter.format_error)(err), delay_unauth_time) | |
592 | ); | |
5ddf8cb1 | 593 | } |
b9903d63 DM |
594 | } |
595 | } | |
d7d23785 | 596 | |
7e21da6e | 597 | match api.find_method(&components[2..], method, &mut uri_param) { |
49d123ee DM |
598 | MethodDefinition::None => { |
599 | let err = http_err!(NOT_FOUND, "Path not found.".to_string()); | |
600 | return Box::new(future::ok((formatter.format_error)(err))); | |
601 | } | |
7e21da6e | 602 | MethodDefinition::Simple(api_method) => { |
f1204833 | 603 | if api_method.protected && env_type == RpcEnvironmentType::PUBLIC { |
4b2cdeb9 | 604 | return proxy_protected_request(api_method, parts, body); |
f1204833 DM |
605 | } else { |
606 | return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param); | |
607 | } | |
7e21da6e | 608 | } |
50cfb695 | 609 | MethodDefinition::Async(async_method) => { |
e82dad97 | 610 | return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param); |
7e21da6e | 611 | } |
9bc17e8d DM |
612 | } |
613 | } | |
614 | } else { | |
7f168523 | 615 | // not Auth required for accessing files! |
9bc17e8d | 616 | |
7d4ef127 DM |
617 | if method != hyper::Method::GET { |
618 | return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported method")))); | |
619 | } | |
620 | ||
f4c514c1 | 621 | if comp_len == 0 { |
7f168523 DM |
622 | let (ticket, token) = extract_auth_data(&parts.headers); |
623 | if ticket != None { | |
624 | match check_auth(&method, &ticket, &token) { | |
7d4ef127 DM |
625 | Ok(username) => { |
626 | let new_token = assemble_csrf_prevention_token(csrf_secret(), &username); | |
627 | return Box::new(future::ok(get_index(Some(username), Some(new_token)))); | |
628 | } | |
91e45873 WB |
629 | _ => { |
630 | return Box::new(delayed_response(get_index(None, None), delay_unauth_time)); | |
631 | } | |
7f168523 DM |
632 | } |
633 | } else { | |
634 | return Box::new(future::ok(get_index(None, None))); | |
635 | } | |
f4c514c1 DM |
636 | } else { |
637 | let filename = api.find_alias(&components); | |
638 | return handle_static_file_download(filename); | |
639 | } | |
9bc17e8d DM |
640 | } |
641 | ||
10be1d29 | 642 | Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string()))) |
9bc17e8d | 643 | } |