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