]>
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 | 8 | |
f7d4e4b5 | 9 | use anyhow::{bail, format_err, Error}; |
ad51d02a | 10 | use futures::future::{self, FutureExt, TryFutureExt}; |
91e45873 | 11 | use futures::stream::TryStreamExt; |
29633e2f | 12 | use hyper::header::{self, HeaderMap}; |
91e45873 | 13 | use hyper::http::request::Parts; |
91e45873 | 14 | use hyper::{Body, Request, Response, StatusCode}; |
29633e2f | 15 | use lazy_static::lazy_static; |
f4c514c1 | 16 | use serde_json::{json, Value}; |
9bc17e8d | 17 | use tokio::fs::File; |
db0cb9ce | 18 | use tokio::time::Instant; |
91e45873 | 19 | use url::form_urlencoded; |
29633e2f | 20 | use regex::Regex; |
9bc17e8d | 21 | |
9ea4bce4 | 22 | use proxmox::http_err; |
4e6dc587 TL |
23 | use proxmox::api::{ |
24 | ApiHandler, | |
25 | ApiMethod, | |
26 | HttpError, | |
0ac61247 | 27 | Permission, |
4e6dc587 TL |
28 | RpcEnvironment, |
29 | RpcEnvironmentType, | |
30 | check_api_permission, | |
31 | }; | |
32 | use proxmox::api::schema::{ | |
33 | ObjectSchema, | |
34 | parse_parameter_strings, | |
35 | parse_simple_value, | |
36 | verify_json_object, | |
37 | }; | |
a2479cfa | 38 | |
91e45873 WB |
39 | use super::environment::RestEnvironment; |
40 | use super::formatter::*; | |
e57e1cd8 DM |
41 | use super::ApiConfig; |
42 | ||
91e45873 | 43 | use crate::auth_helpers::*; |
e7cb4dc5 | 44 | use crate::api2::types::Userid; |
91e45873 | 45 | use crate::tools; |
72dc6832 | 46 | use crate::tools::ticket::Ticket; |
4b40148c | 47 | use crate::config::cached_user_info::CachedUserInfo; |
9bc17e8d | 48 | |
4b2cdeb9 DM |
49 | extern "C" { fn tzset(); } |
50 | ||
f0b10921 DM |
51 | pub struct RestServer { |
52 | pub api_config: Arc<ApiConfig>, | |
53 | } | |
54 | ||
4703ba81 TL |
55 | const MAX_URI_QUERY_LENGTH: usize = 3072; |
56 | ||
f0b10921 DM |
57 | impl RestServer { |
58 | ||
59 | pub fn new(api_config: ApiConfig) -> Self { | |
60 | Self { api_config: Arc::new(api_config) } | |
61 | } | |
62 | } | |
63 | ||
91e45873 WB |
64 | impl tower_service::Service<&tokio_openssl::SslStream<tokio::net::TcpStream>> for RestServer { |
65 | type Response = ApiService; | |
7fb4f564 | 66 | type Error = Error; |
91e45873 WB |
67 | type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>; |
68 | ||
69 | fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> { | |
70 | Poll::Ready(Ok(())) | |
71 | } | |
72 | ||
73 | fn call(&mut self, ctx: &tokio_openssl::SslStream<tokio::net::TcpStream>) -> Self::Future { | |
74 | match ctx.get_ref().peer_addr() { | |
80af0467 | 75 | Err(err) => { |
91e45873 | 76 | future::err(format_err!("unable to get peer address - {}", err)).boxed() |
80af0467 DM |
77 | } |
78 | Ok(peer) => { | |
91e45873 | 79 | future::ok(ApiService { peer, api_config: self.api_config.clone() }).boxed() |
80af0467 DM |
80 | } |
81 | } | |
f0b10921 DM |
82 | } |
83 | } | |
84 | ||
91e45873 WB |
85 | impl tower_service::Service<&tokio::net::TcpStream> for RestServer { |
86 | type Response = ApiService; | |
7fb4f564 | 87 | type Error = Error; |
91e45873 WB |
88 | type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>; |
89 | ||
90 | fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> { | |
91 | Poll::Ready(Ok(())) | |
92 | } | |
93 | ||
94 | fn call(&mut self, ctx: &tokio::net::TcpStream) -> Self::Future { | |
80af0467 DM |
95 | match ctx.peer_addr() { |
96 | Err(err) => { | |
91e45873 | 97 | future::err(format_err!("unable to get peer address - {}", err)).boxed() |
80af0467 DM |
98 | } |
99 | Ok(peer) => { | |
91e45873 | 100 | future::ok(ApiService { peer, api_config: self.api_config.clone() }).boxed() |
80af0467 DM |
101 | } |
102 | } | |
7fb4f564 DM |
103 | } |
104 | } | |
105 | ||
f0b10921 | 106 | pub struct ApiService { |
7fb4f564 | 107 | pub peer: std::net::SocketAddr, |
f0b10921 DM |
108 | pub api_config: Arc<ApiConfig>, |
109 | } | |
110 | ||
7fb4f564 DM |
111 | fn log_response( |
112 | peer: &std::net::SocketAddr, | |
113 | method: hyper::Method, | |
400c568f | 114 | path_query: &str, |
7fb4f564 DM |
115 | resp: &Response<Body>, |
116 | ) { | |
7e03988c | 117 | |
d4736445 | 118 | if resp.extensions().get::<NoLogExtension>().is_some() { return; }; |
7e03988c | 119 | |
4703ba81 TL |
120 | // we also log URL-to-long requests, so avoid message bigger than PIPE_BUF (4k on Linux) |
121 | // to profit from atomicty guarantees for O_APPEND opened logfiles | |
400c568f | 122 | let path = &path_query[..MAX_URI_QUERY_LENGTH.min(path_query.len())]; |
4703ba81 | 123 | |
d4736445 | 124 | let status = resp.status(); |
7e03988c | 125 | |
1133fe9a | 126 | if !(status.is_success() || status.is_informational()) { |
d4736445 | 127 | let reason = status.canonical_reason().unwrap_or("unknown reason"); |
44c00c0d | 128 | |
d4736445 DM |
129 | let mut message = "request failed"; |
130 | if let Some(data) = resp.extensions().get::<ErrorMessageExtension>() { | |
131 | message = &data.0; | |
78a1fa67 | 132 | } |
d4736445 | 133 | |
7fb4f564 | 134 | log::error!("{} {}: {} {}: [client {}] {}", method.as_str(), path, status.as_str(), reason, peer, message); |
78a1fa67 DM |
135 | } |
136 | } | |
f0b10921 | 137 | |
29633e2f TL |
138 | fn get_proxied_peer(headers: &HeaderMap) -> Option<std::net::SocketAddr> { |
139 | lazy_static! { | |
140 | static ref RE: Regex = Regex::new(r#"for="([^"]+)""#).unwrap(); | |
141 | } | |
142 | let forwarded = headers.get(header::FORWARDED)?.to_str().ok()?; | |
143 | let capture = RE.captures(&forwarded)?; | |
144 | let rhost = capture.get(1)?.as_str(); | |
145 | ||
146 | rhost.parse().ok() | |
147 | } | |
148 | ||
91e45873 WB |
149 | impl tower_service::Service<Request<Body>> for ApiService { |
150 | type Response = Response<Body>; | |
7fb4f564 | 151 | type Error = Error; |
91e45873 WB |
152 | type Future = Pin<Box<dyn Future<Output = Result<Response<Body>, Self::Error>> + Send>>; |
153 | ||
154 | fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> { | |
155 | Poll::Ready(Ok(())) | |
156 | } | |
f0b10921 | 157 | |
91e45873 | 158 | fn call(&mut self, req: Request<Body>) -> Self::Future { |
400c568f | 159 | let path = req.uri().path_and_query().unwrap().as_str().to_owned(); |
d4736445 DM |
160 | let method = req.method().clone(); |
161 | ||
07995a3c | 162 | let config = Arc::clone(&self.api_config); |
29633e2f TL |
163 | let peer = match get_proxied_peer(req.headers()) { |
164 | Some(proxied_peer) => proxied_peer, | |
165 | None => self.peer, | |
166 | }; | |
07995a3c | 167 | async move { |
29633e2f | 168 | let response = match handle_request(config, req, &peer).await { |
b947b1e7 | 169 | Ok(response) => response, |
f0b10921 | 170 | Err(err) => { |
b947b1e7 TL |
171 | let (err, code) = match err.downcast_ref::<HttpError>() { |
172 | Some(apierr) => (apierr.message.clone(), apierr.code), | |
173 | _ => (err.to_string(), StatusCode::BAD_REQUEST), | |
174 | }; | |
175 | Response::builder().status(code).body(err.into())? | |
f0b10921 | 176 | } |
b947b1e7 TL |
177 | }; |
178 | log_response(&peer, method, &path, &response); | |
179 | Ok(response) | |
07995a3c TL |
180 | } |
181 | .boxed() | |
f0b10921 DM |
182 | } |
183 | } | |
184 | ||
70fbac84 DM |
185 | fn parse_query_parameters<S: 'static + BuildHasher + Send>( |
186 | param_schema: &ObjectSchema, | |
187 | form: &str, // x-www-form-urlencoded body data | |
188 | parts: &Parts, | |
189 | uri_param: &HashMap<String, String, S>, | |
190 | ) -> Result<Value, Error> { | |
191 | ||
192 | let mut param_list: Vec<(String, String)> = vec![]; | |
193 | ||
194 | if !form.is_empty() { | |
195 | for (k, v) in form_urlencoded::parse(form.as_bytes()).into_owned() { | |
196 | param_list.push((k, v)); | |
197 | } | |
198 | } | |
199 | ||
200 | if let Some(query_str) = parts.uri.query() { | |
201 | for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() { | |
202 | if k == "_dc" { continue; } // skip extjs "disable cache" parameter | |
203 | param_list.push((k, v)); | |
204 | } | |
205 | } | |
206 | ||
207 | for (k, v) in uri_param { | |
208 | param_list.push((k.clone(), v.clone())); | |
209 | } | |
210 | ||
211 | let params = parse_parameter_strings(¶m_list, param_schema, true)?; | |
212 | ||
213 | Ok(params) | |
214 | } | |
215 | ||
2bbd835b | 216 | async fn get_request_parameters<S: 'static + BuildHasher + Send>( |
75a5a689 | 217 | param_schema: &ObjectSchema, |
9bc17e8d DM |
218 | parts: Parts, |
219 | req_body: Body, | |
62ee2eb4 | 220 | uri_param: HashMap<String, String, S>, |
ad51d02a DM |
221 | ) -> Result<Value, Error> { |
222 | ||
0ffbccce DM |
223 | let mut is_json = false; |
224 | ||
225 | if let Some(value) = parts.headers.get(header::CONTENT_TYPE) { | |
8346f0d5 DM |
226 | match value.to_str().map(|v| v.split(';').next()) { |
227 | Ok(Some("application/x-www-form-urlencoded")) => { | |
228 | is_json = false; | |
229 | } | |
230 | Ok(Some("application/json")) => { | |
231 | is_json = true; | |
232 | } | |
ad51d02a | 233 | _ => bail!("unsupported content type {:?}", value.to_str()), |
0ffbccce DM |
234 | } |
235 | } | |
236 | ||
ad51d02a | 237 | let body = req_body |
8aa67ee7 | 238 | .map_err(|err| http_err!(BAD_REQUEST, "Promlems reading request body: {}", err)) |
91e45873 | 239 | .try_fold(Vec::new(), |mut acc, chunk| async move { |
9bc17e8d DM |
240 | if acc.len() + chunk.len() < 64*1024 { //fimxe: max request body size? |
241 | acc.extend_from_slice(&*chunk); | |
242 | Ok(acc) | |
91e45873 | 243 | } else { |
8aa67ee7 | 244 | Err(http_err!(BAD_REQUEST, "Request body too large")) |
9bc17e8d | 245 | } |
ad51d02a | 246 | }).await?; |
9bc17e8d | 247 | |
70fbac84 | 248 | let utf8_data = std::str::from_utf8(&body) |
ad51d02a | 249 | .map_err(|err| format_err!("Request body not uft8: {}", err))?; |
0ffbccce | 250 | |
ad51d02a | 251 | if is_json { |
70fbac84 | 252 | let mut params: Value = serde_json::from_str(utf8_data)?; |
ad51d02a | 253 | for (k, v) in uri_param { |
75a5a689 | 254 | if let Some((_optional, prop_schema)) = param_schema.lookup(&k) { |
ad51d02a | 255 | params[&k] = parse_simple_value(&v, prop_schema)?; |
9bc17e8d | 256 | } |
ad51d02a | 257 | } |
75a5a689 | 258 | verify_json_object(¶ms, param_schema)?; |
ad51d02a | 259 | return Ok(params); |
70fbac84 DM |
260 | } else { |
261 | parse_query_parameters(param_schema, utf8_data, &parts, &uri_param) | |
ad51d02a | 262 | } |
9bc17e8d DM |
263 | } |
264 | ||
7171b3e0 DM |
265 | struct NoLogExtension(); |
266 | ||
ad51d02a | 267 | async fn proxy_protected_request( |
4b2cdeb9 | 268 | info: &'static ApiMethod, |
a3da38dd | 269 | mut parts: Parts, |
f1204833 | 270 | req_body: Body, |
29633e2f | 271 | peer: &std::net::SocketAddr, |
ad51d02a | 272 | ) -> Result<Response<Body>, Error> { |
f1204833 | 273 | |
a3da38dd DM |
274 | let mut uri_parts = parts.uri.clone().into_parts(); |
275 | ||
276 | uri_parts.scheme = Some(http::uri::Scheme::HTTP); | |
277 | uri_parts.authority = Some(http::uri::Authority::from_static("127.0.0.1:82")); | |
278 | let new_uri = http::Uri::from_parts(uri_parts).unwrap(); | |
279 | ||
280 | parts.uri = new_uri; | |
281 | ||
29633e2f TL |
282 | let mut request = Request::from_parts(parts, req_body); |
283 | request | |
284 | .headers_mut() | |
285 | .insert(header::FORWARDED, format!("for=\"{}\";", peer).parse().unwrap()); | |
a3da38dd | 286 | |
ad51d02a DM |
287 | let reload_timezone = info.reload_timezone; |
288 | ||
a3da38dd DM |
289 | let resp = hyper::client::Client::new() |
290 | .request(request) | |
fc7f0352 | 291 | .map_err(Error::from) |
91e45873 | 292 | .map_ok(|mut resp| { |
1cb99c23 | 293 | resp.extensions_mut().insert(NoLogExtension()); |
7e03988c | 294 | resp |
ad51d02a DM |
295 | }) |
296 | .await?; | |
a3da38dd | 297 | |
ad51d02a | 298 | if reload_timezone { unsafe { tzset(); } } |
1cb99c23 | 299 | |
ad51d02a | 300 | Ok(resp) |
f1204833 DM |
301 | } |
302 | ||
70fbac84 | 303 | pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher + Send>( |
f757b30e | 304 | mut rpcenv: Env, |
279ecfdf | 305 | info: &'static ApiMethod, |
1571873d | 306 | formatter: &'static OutputFormatter, |
9bc17e8d DM |
307 | parts: Parts, |
308 | req_body: Body, | |
62ee2eb4 | 309 | uri_param: HashMap<String, String, S>, |
ad51d02a DM |
310 | ) -> Result<Response<Body>, Error> { |
311 | ||
a154a8e8 DM |
312 | let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); |
313 | ||
70fbac84 | 314 | let result = match info.handler { |
329d40b5 | 315 | ApiHandler::AsyncHttp(handler) => { |
70fbac84 DM |
316 | let params = parse_query_parameters(info.parameters, "", &parts, &uri_param)?; |
317 | (handler)(parts, req_body, params, info, Box::new(rpcenv)).await | |
318 | } | |
319 | ApiHandler::Sync(handler) => { | |
320 | let params = get_request_parameters(info.parameters, parts, req_body, uri_param).await?; | |
321 | (handler)(params, info, &mut rpcenv) | |
322 | .map(|data| (formatter.format_data)(data, &rpcenv)) | |
323 | } | |
bb084b9c DM |
324 | ApiHandler::Async(handler) => { |
325 | let params = get_request_parameters(info.parameters, parts, req_body, uri_param).await?; | |
326 | (handler)(params, info, &mut rpcenv) | |
327 | .await | |
328 | .map(|data| (formatter.format_data)(data, &rpcenv)) | |
329 | } | |
70fbac84 | 330 | }; |
a154a8e8 | 331 | |
70fbac84 DM |
332 | let resp = match result { |
333 | Ok(resp) => resp, | |
ad51d02a DM |
334 | Err(err) => { |
335 | if let Some(httperr) = err.downcast_ref::<HttpError>() { | |
336 | if httperr.code == StatusCode::UNAUTHORIZED { | |
db0cb9ce | 337 | tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await; |
ad51d02a | 338 | } |
4b2cdeb9 | 339 | } |
ad51d02a DM |
340 | (formatter.format_error)(err) |
341 | } | |
342 | }; | |
4b2cdeb9 | 343 | |
ad51d02a DM |
344 | if info.reload_timezone { unsafe { tzset(); } } |
345 | ||
ad51d02a | 346 | Ok(resp) |
7e21da6e DM |
347 | } |
348 | ||
abd4c4cb TL |
349 | fn get_index( |
350 | userid: Option<Userid>, | |
6c5bdef5 | 351 | csrf_token: Option<String>, |
abd4c4cb TL |
352 | language: Option<String>, |
353 | api: &Arc<ApiConfig>, | |
354 | parts: Parts, | |
355 | ) -> Response<Body> { | |
f4c514c1 | 356 | |
f69adc81 | 357 | let nodename = proxmox::tools::nodename(); |
6c5bdef5 | 358 | let user = userid.as_ref().map(|u| u.as_str()).unwrap_or(""); |
7f168523 | 359 | |
6c5bdef5 | 360 | let csrf_token = csrf_token.unwrap_or_else(|| String::from("")); |
f4c514c1 | 361 | |
f9e3b110 | 362 | let mut debug = false; |
01ca99da | 363 | let mut template_file = "index"; |
f9e3b110 DC |
364 | |
365 | if let Some(query_str) = parts.uri.query() { | |
366 | for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() { | |
3f683799 | 367 | if k == "debug" && v != "0" && v != "false" { |
f9e3b110 | 368 | debug = true; |
01ca99da DC |
369 | } else if k == "console" { |
370 | template_file = "console"; | |
f9e3b110 DC |
371 | } |
372 | } | |
373 | } | |
374 | ||
abd4c4cb TL |
375 | let mut lang = String::from(""); |
376 | if let Some(language) = language { | |
377 | if Path::new(&format!("/usr/share/pbs-i18n/pbs-lang-{}.js", language)).exists() { | |
378 | lang = language; | |
379 | } | |
380 | } | |
381 | ||
f9e3b110 | 382 | let data = json!({ |
f4c514c1 | 383 | "NodeName": nodename, |
6c5bdef5 TL |
384 | "UserName": user, |
385 | "CSRFPreventionToken": csrf_token, | |
abd4c4cb | 386 | "language": lang, |
f9e3b110 | 387 | "debug": debug, |
f4c514c1 DM |
388 | }); |
389 | ||
adfcfb67 TL |
390 | let (ct, index) = match api.render_template(template_file, &data) { |
391 | Ok(index) => ("text/html", index), | |
f9e3b110 | 392 | Err(err) => { |
adfcfb67 | 393 | ("text/plain", format!("Error rendering template: {}", err)) |
01ca99da | 394 | } |
f9e3b110 | 395 | }; |
f4c514c1 | 396 | |
7f168523 | 397 | Response::builder() |
d15009c0 | 398 | .status(StatusCode::OK) |
f9e3b110 | 399 | .header(header::CONTENT_TYPE, ct) |
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 | |
8aa67ee7 | 441 | .map_err(|err| http_err!(BAD_REQUEST, "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 | |
8aa67ee7 | 446 | .map_err(|err| http_err!(BAD_REQUEST, "File read failed: {}", err))?; |
91e45873 WB |
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 | |
8aa67ee7 | 460 | .map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?; |
91e45873 | 461 | |
db0cb9ce WB |
462 | let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new()) |
463 | .map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze())); | |
91e45873 WB |
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 | ||
ad51d02a | 475 | async fn handle_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> { |
9bc17e8d | 476 | |
9c18e935 | 477 | let metadata = tokio::fs::metadata(filename.clone()) |
8aa67ee7 | 478 | .map_err(|err| http_err!(BAD_REQUEST, "File access problems: {}", err)) |
9c18e935 TL |
479 | .await?; |
480 | ||
481 | if metadata.len() < 1024*32 { | |
482 | simple_static_file_download(filename).await | |
483 | } else { | |
484 | chuncked_static_file_download(filename).await | |
485 | } | |
9bc17e8d DM |
486 | } |
487 | ||
abd4c4cb | 488 | fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>, Option<String>) { |
5ddf8cb1 DM |
489 | |
490 | let mut ticket = None; | |
abd4c4cb | 491 | let mut language = None; |
5ddf8cb1 DM |
492 | if let Some(raw_cookie) = headers.get("COOKIE") { |
493 | if let Ok(cookie) = raw_cookie.to_str() { | |
09f12d1c | 494 | ticket = tools::extract_cookie(cookie, "PBSAuthCookie"); |
abd4c4cb | 495 | language = tools::extract_cookie(cookie, "PBSLangCookie"); |
5ddf8cb1 DM |
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 | ||
abd4c4cb | 504 | (ticket, token, language) |
5ddf8cb1 DM |
505 | } |
506 | ||
4b40148c DM |
507 | fn check_auth( |
508 | method: &hyper::Method, | |
509 | ticket: &Option<String>, | |
510 | token: &Option<String>, | |
511 | user_info: &CachedUserInfo, | |
e7cb4dc5 | 512 | ) -> Result<Userid, Error> { |
e5662b04 | 513 | let ticket_lifetime = tools::ticket::TICKET_LIFETIME; |
5ddf8cb1 | 514 | |
72dc6832 WB |
515 | let ticket = ticket.as_ref().map(String::as_str); |
516 | let userid: Userid = Ticket::parse(&ticket.ok_or_else(|| format_err!("missing ticket"))?)? | |
517 | .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?; | |
5ddf8cb1 | 518 | |
e7cb4dc5 | 519 | if !user_info.is_active_user(&userid) { |
4b40148c DM |
520 | bail!("user account disabled or expired."); |
521 | } | |
522 | ||
5ddf8cb1 DM |
523 | if method != hyper::Method::GET { |
524 | if let Some(token) = token { | |
e7cb4dc5 | 525 | verify_csrf_prevention_token(csrf_secret(), &userid, &token, -300, ticket_lifetime)?; |
5ddf8cb1 | 526 | } else { |
8225aa2f | 527 | bail!("missing CSRF prevention token"); |
5ddf8cb1 DM |
528 | } |
529 | } | |
530 | ||
e7cb4dc5 | 531 | Ok(userid) |
5ddf8cb1 DM |
532 | } |
533 | ||
29633e2f TL |
534 | async fn handle_request( |
535 | api: Arc<ApiConfig>, | |
536 | req: Request<Body>, | |
537 | peer: &std::net::SocketAddr, | |
538 | ) -> Result<Response<Body>, Error> { | |
141de837 DM |
539 | |
540 | let (parts, body) = req.into_parts(); | |
141de837 | 541 | let method = parts.method.clone(); |
217c22c7 | 542 | let (path, components) = tools::normalize_uri_path(parts.uri.path())?; |
141de837 | 543 | |
9bc17e8d DM |
544 | let comp_len = components.len(); |
545 | ||
4703ba81 TL |
546 | let query = parts.uri.query().unwrap_or_default(); |
547 | if path.len() + query.len() > MAX_URI_QUERY_LENGTH { | |
548 | return Ok(Response::builder() | |
549 | .status(StatusCode::URI_TOO_LONG) | |
550 | .body("".into()) | |
551 | .unwrap()); | |
552 | } | |
553 | ||
f1204833 DM |
554 | let env_type = api.env_type(); |
555 | let mut rpcenv = RestEnvironment::new(env_type); | |
e82dad97 | 556 | |
29633e2f TL |
557 | rpcenv.set_client_ip(Some(*peer)); |
558 | ||
4b40148c DM |
559 | let user_info = CachedUserInfo::new()?; |
560 | ||
b9903d63 | 561 | let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); |
9989d2c4 | 562 | let access_forbidden_time = std::time::Instant::now() + std::time::Duration::from_millis(500); |
b9903d63 | 563 | |
576e3bf2 | 564 | if comp_len >= 1 && components[0] == "api2" { |
5ddf8cb1 | 565 | |
9bc17e8d | 566 | if comp_len >= 2 { |
ad51d02a | 567 | |
9bc17e8d | 568 | let format = components[1]; |
ad51d02a | 569 | |
1571873d DM |
570 | let formatter = match format { |
571 | "json" => &JSON_FORMATTER, | |
572 | "extjs" => &EXTJS_FORMATTER, | |
ad51d02a | 573 | _ => bail!("Unsupported output format '{}'.", format), |
1571873d | 574 | }; |
9bc17e8d | 575 | |
e7ea17de | 576 | let mut uri_param = HashMap::new(); |
0ac61247 | 577 | let api_method = api.find_method(&components[2..], method.clone(), &mut uri_param); |
e7ea17de | 578 | |
0ac61247 TL |
579 | let mut auth_required = true; |
580 | if let Some(api_method) = api_method { | |
581 | if let Permission::World = *api_method.access.permission { | |
582 | auth_required = false; // no auth for endpoints with World permission | |
583 | } | |
584 | } | |
585 | ||
586 | if auth_required { | |
abd4c4cb | 587 | let (ticket, token, _) = extract_auth_data(&parts.headers); |
4b40148c | 588 | match check_auth(&method, &ticket, &token, &user_info) { |
e7cb4dc5 | 589 | Ok(userid) => rpcenv.set_user(Some(userid.to_string())), |
5ddf8cb1 DM |
590 | Err(err) => { |
591 | // always delay unauthorized calls by 3 seconds (from start of request) | |
8aa67ee7 | 592 | let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err); |
db0cb9ce | 593 | tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await; |
ad51d02a | 594 | return Ok((formatter.format_error)(err)); |
5ddf8cb1 | 595 | } |
b9903d63 DM |
596 | } |
597 | } | |
d7d23785 | 598 | |
0ac61247 | 599 | match api_method { |
255f378a | 600 | None => { |
8aa67ee7 | 601 | let err = http_err!(NOT_FOUND, "Path '{}' not found.", path); |
ad51d02a | 602 | return Ok((formatter.format_error)(err)); |
49d123ee | 603 | } |
255f378a | 604 | Some(api_method) => { |
4b40148c | 605 | let user = rpcenv.get_user(); |
6e695960 | 606 | if !check_api_permission(api_method.access.permission, user.as_deref(), &uri_param, user_info.as_ref()) { |
8aa67ee7 | 607 | let err = http_err!(FORBIDDEN, "permission check failed"); |
9989d2c4 | 608 | tokio::time::delay_until(Instant::from_std(access_forbidden_time)).await; |
4b40148c DM |
609 | return Ok((formatter.format_error)(err)); |
610 | } | |
611 | ||
4299ca72 | 612 | let result = if api_method.protected && env_type == RpcEnvironmentType::PUBLIC { |
29633e2f | 613 | proxy_protected_request(api_method, parts, body, peer).await |
f1204833 | 614 | } else { |
4299ca72 DM |
615 | handle_api_request(rpcenv, api_method, formatter, parts, body, uri_param).await |
616 | }; | |
617 | ||
618 | if let Err(err) = result { | |
619 | return Ok((formatter.format_error)(err)); | |
f1204833 | 620 | } |
4299ca72 | 621 | return result; |
7e21da6e | 622 | } |
9bc17e8d | 623 | } |
4299ca72 | 624 | |
9bc17e8d | 625 | } |
ad51d02a | 626 | } else { |
7f168523 | 627 | // not Auth required for accessing files! |
9bc17e8d | 628 | |
7d4ef127 | 629 | if method != hyper::Method::GET { |
ad51d02a | 630 | bail!("Unsupported HTTP method {}", method); |
7d4ef127 DM |
631 | } |
632 | ||
f4c514c1 | 633 | if comp_len == 0 { |
abd4c4cb | 634 | let (ticket, token, language) = extract_auth_data(&parts.headers); |
7f168523 | 635 | if ticket != None { |
4b40148c | 636 | match check_auth(&method, &ticket, &token, &user_info) { |
e7cb4dc5 WB |
637 | Ok(userid) => { |
638 | let new_token = assemble_csrf_prevention_token(csrf_secret(), &userid); | |
abd4c4cb | 639 | return Ok(get_index(Some(userid), Some(new_token), language, &api, parts)); |
7d4ef127 | 640 | } |
91e45873 | 641 | _ => { |
db0cb9ce | 642 | tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await; |
abd4c4cb | 643 | return Ok(get_index(None, None, language, &api, parts)); |
91e45873 | 644 | } |
7f168523 DM |
645 | } |
646 | } else { | |
abd4c4cb | 647 | return Ok(get_index(None, None, language, &api, parts)); |
7f168523 | 648 | } |
f4c514c1 DM |
649 | } else { |
650 | let filename = api.find_alias(&components); | |
ad51d02a | 651 | return handle_static_file_download(filename).await; |
f4c514c1 | 652 | } |
9bc17e8d DM |
653 | } |
654 | ||
8aa67ee7 | 655 | Err(http_err!(NOT_FOUND, "Path '{}' not found.", path)) |
9bc17e8d | 656 | } |