]> git.proxmox.com Git - proxmox-backup.git/blame - proxmox-rest-server/src/rest.rs
proxmox-rest-server: cleanup formatter, improve docs
[proxmox-backup.git] / proxmox-rest-server / src / rest.rs
CommitLineData
91e45873 1use std::collections::HashMap;
db0cb9ce 2use std::future::Future;
62ee2eb4 3use std::hash::BuildHasher;
826bb982 4use std::path::{Path, PathBuf};
91e45873 5use std::pin::Pin;
8e7e2223 6use std::sync::{Arc, Mutex};
91e45873 7use std::task::{Context, Poll};
9bc17e8d 8
f7d4e4b5 9use anyhow::{bail, format_err, Error};
ad51d02a 10use futures::future::{self, FutureExt, TryFutureExt};
91e45873 11use futures::stream::TryStreamExt;
8e7e2223 12use hyper::body::HttpBody;
d43c407a 13use hyper::header::{self, HeaderMap};
91e45873 14use hyper::http::request::Parts;
91e45873 15use hyper::{Body, Request, Response, StatusCode};
29633e2f 16use lazy_static::lazy_static;
d43c407a 17use regex::Regex;
7fa9a37c 18use serde_json::Value;
9bc17e8d 19use tokio::fs::File;
db0cb9ce 20use tokio::time::Instant;
91e45873 21use url::form_urlencoded;
9bc17e8d 22
4e6dc587 23use proxmox::api::schema::{
d43c407a 24 parse_parameter_strings, parse_simple_value, verify_json_object, ObjectSchemaType,
29a59b38 25 ParameterSchema,
4e6dc587 26};
d43c407a
TL
27use proxmox::api::{
28 check_api_permission, ApiHandler, ApiMethod, HttpError, Permission, RpcEnvironment,
98b7d58b 29 RpcEnvironmentType, UserInformation,
d43c407a
TL
30};
31use proxmox::http_err;
a2479cfa 32
2b7f8dd5 33use pbs_tools::compression::{DeflateEncoder, Level};
fc5870be 34use pbs_tools::stream::AsyncReaderStream;
6fbf0acc
DM
35
36use crate::{
36b7085e 37 ApiConfig, FileLogger, AuthError, RestEnvironment, CompressionMethod,
6fbf0acc 38 extract_cookie, normalize_uri_path, formatter::*,
605fe2e7 39};
e57e1cd8 40
d43c407a
TL
41extern "C" {
42 fn tzset();
43}
4b2cdeb9 44
1b1a5537
DM
45struct AuthStringExtension(String);
46
98b7d58b
DM
47struct EmptyUserInformation {}
48
49impl UserInformation for EmptyUserInformation {
50 fn is_superuser(&self, _userid: &str) -> bool { false }
51 fn is_group_member(&self, _userid: &str, _group: &str) -> bool { false }
52 fn lookup_privs(&self, _userid: &str, _path: &[&str]) -> u64 { 0 }
53}
54
f0b10921
DM
55pub struct RestServer {
56 pub api_config: Arc<ApiConfig>,
57}
58
4703ba81 59const MAX_URI_QUERY_LENGTH: usize = 3072;
59477ad2 60const CHUNK_SIZE_LIMIT: u64 = 32 * 1024;
4703ba81 61
f0b10921 62impl RestServer {
f0b10921 63 pub fn new(api_config: ApiConfig) -> Self {
d43c407a
TL
64 Self {
65 api_config: Arc::new(api_config),
66 }
f0b10921
DM
67 }
68}
69
d43c407a
TL
70impl tower_service::Service<&Pin<Box<tokio_openssl::SslStream<tokio::net::TcpStream>>>>
71 for RestServer
72{
91e45873 73 type Response = ApiService;
7fb4f564 74 type Error = Error;
91e45873
WB
75 type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>;
76
77 fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> {
78 Poll::Ready(Ok(()))
79 }
80
d43c407a
TL
81 fn call(
82 &mut self,
83 ctx: &Pin<Box<tokio_openssl::SslStream<tokio::net::TcpStream>>>,
84 ) -> Self::Future {
91e45873 85 match ctx.get_ref().peer_addr() {
d43c407a
TL
86 Err(err) => future::err(format_err!("unable to get peer address - {}", err)).boxed(),
87 Ok(peer) => future::ok(ApiService {
88 peer,
89 api_config: self.api_config.clone(),
90 })
91 .boxed(),
80af0467 92 }
f0b10921
DM
93 }
94}
95
91e45873
WB
96impl tower_service::Service<&tokio::net::TcpStream> for RestServer {
97 type Response = ApiService;
7fb4f564 98 type Error = Error;
91e45873
WB
99 type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>;
100
101 fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> {
102 Poll::Ready(Ok(()))
103 }
104
105 fn call(&mut self, ctx: &tokio::net::TcpStream) -> Self::Future {
80af0467 106 match ctx.peer_addr() {
d43c407a
TL
107 Err(err) => future::err(format_err!("unable to get peer address - {}", err)).boxed(),
108 Ok(peer) => future::ok(ApiService {
109 peer,
110 api_config: self.api_config.clone(),
111 })
112 .boxed(),
80af0467 113 }
7fb4f564
DM
114 }
115}
116
b57c0dbe
SR
117impl tower_service::Service<&tokio::net::UnixStream> for RestServer {
118 type Response = ApiService;
119 type Error = Error;
120 type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>;
121
122 fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> {
123 Poll::Ready(Ok(()))
124 }
125
126 fn call(&mut self, _ctx: &tokio::net::UnixStream) -> Self::Future {
127 // TODO: Find a way to actually represent the vsock peer in the ApiService struct - for now
128 // it doesn't really matter, so just use a fake IP address
129 let fake_peer = "0.0.0.0:807".parse().unwrap();
130 future::ok(ApiService {
131 peer: fake_peer,
d43c407a
TL
132 api_config: self.api_config.clone(),
133 })
134 .boxed()
b57c0dbe
SR
135 }
136}
137
f0b10921 138pub struct ApiService {
7fb4f564 139 pub peer: std::net::SocketAddr,
f0b10921
DM
140 pub api_config: Arc<ApiConfig>,
141}
142
7fb4f564 143fn log_response(
fe4cc5b1 144 logfile: Option<&Arc<Mutex<FileLogger>>>,
7fb4f564
DM
145 peer: &std::net::SocketAddr,
146 method: hyper::Method,
400c568f 147 path_query: &str,
7fb4f564 148 resp: &Response<Body>,
86f3c236 149 user_agent: Option<String>,
7fb4f564 150) {
d43c407a
TL
151 if resp.extensions().get::<NoLogExtension>().is_some() {
152 return;
153 };
7e03988c 154
4703ba81
TL
155 // we also log URL-to-long requests, so avoid message bigger than PIPE_BUF (4k on Linux)
156 // to profit from atomicty guarantees for O_APPEND opened logfiles
400c568f 157 let path = &path_query[..MAX_URI_QUERY_LENGTH.min(path_query.len())];
4703ba81 158
d4736445 159 let status = resp.status();
1133fe9a 160 if !(status.is_success() || status.is_informational()) {
d4736445 161 let reason = status.canonical_reason().unwrap_or("unknown reason");
44c00c0d 162
6b5013ed
TL
163 let message = match resp.extensions().get::<ErrorMessageExtension>() {
164 Some(data) => &data.0,
165 None => "request failed",
166 };
d4736445 167
d43c407a
TL
168 log::error!(
169 "{} {}: {} {}: [client {}] {}",
170 method.as_str(),
171 path,
172 status.as_str(),
173 reason,
174 peer,
175 message
176 );
78a1fa67 177 }
8e7e2223 178 if let Some(logfile) = logfile {
1b1a5537
DM
179 let auth_id = match resp.extensions().get::<AuthStringExtension>() {
180 Some(AuthStringExtension(auth_id)) => auth_id.clone(),
e6dc35ac 181 None => "-".to_string(),
8e7e2223
TL
182 };
183 let now = proxmox::tools::time::epoch_i64();
184 // time format which apache/nginx use (by default), copied from pve-http-server
185 let datetime = proxmox::tools::time::strftime_local("%d/%m/%Y:%H:%M:%S %z", now)
e062ebbc 186 .unwrap_or_else(|_| "-".to_string());
8e7e2223 187
d43c407a
TL
188 logfile.lock().unwrap().log(format!(
189 "{} - {} [{}] \"{} {}\" {} {} {}",
190 peer.ip(),
191 auth_id,
192 datetime,
193 method.as_str(),
194 path,
195 status.as_str(),
196 resp.body().size_hint().lower(),
197 user_agent.unwrap_or_else(|| "-".to_string()),
198 ));
8e7e2223 199 }
78a1fa67 200}
1b1a5537 201
29633e2f
TL
202fn get_proxied_peer(headers: &HeaderMap) -> Option<std::net::SocketAddr> {
203 lazy_static! {
204 static ref RE: Regex = Regex::new(r#"for="([^"]+)""#).unwrap();
205 }
206 let forwarded = headers.get(header::FORWARDED)?.to_str().ok()?;
207 let capture = RE.captures(&forwarded)?;
208 let rhost = capture.get(1)?.as_str();
209
210 rhost.parse().ok()
211}
212
86f3c236
TL
213fn get_user_agent(headers: &HeaderMap) -> Option<String> {
214 let agent = headers.get(header::USER_AGENT)?.to_str();
d43c407a
TL
215 agent
216 .map(|s| {
217 let mut s = s.to_owned();
218 s.truncate(128);
219 s
220 })
221 .ok()
86f3c236
TL
222}
223
91e45873
WB
224impl tower_service::Service<Request<Body>> for ApiService {
225 type Response = Response<Body>;
7fb4f564 226 type Error = Error;
12e874ce
FG
227 #[allow(clippy::type_complexity)]
228 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
91e45873
WB
229
230 fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> {
231 Poll::Ready(Ok(()))
232 }
f0b10921 233
91e45873 234 fn call(&mut self, req: Request<Body>) -> Self::Future {
400c568f 235 let path = req.uri().path_and_query().unwrap().as_str().to_owned();
d4736445 236 let method = req.method().clone();
86f3c236 237 let user_agent = get_user_agent(req.headers());
d4736445 238
07995a3c 239 let config = Arc::clone(&self.api_config);
29633e2f
TL
240 let peer = match get_proxied_peer(req.headers()) {
241 Some(proxied_peer) => proxied_peer,
242 None => self.peer,
243 };
07995a3c 244 async move {
8e7e2223 245 let response = match handle_request(Arc::clone(&config), req, &peer).await {
b947b1e7 246 Ok(response) => response,
f0b10921 247 Err(err) => {
b947b1e7
TL
248 let (err, code) = match err.downcast_ref::<HttpError>() {
249 Some(apierr) => (apierr.message.clone(), apierr.code),
250 _ => (err.to_string(), StatusCode::BAD_REQUEST),
251 };
f4d371d2
TL
252 Response::builder()
253 .status(code)
254 .extension(ErrorMessageExtension(err.to_string()))
255 .body(err.into())?
f0b10921 256 }
b947b1e7 257 };
36b7085e 258 let logger = config.get_access_log();
86f3c236 259 log_response(logger, &peer, method, &path, &response, user_agent);
b947b1e7 260 Ok(response)
07995a3c
TL
261 }
262 .boxed()
f0b10921
DM
263 }
264}
265
70fbac84 266fn parse_query_parameters<S: 'static + BuildHasher + Send>(
b2362a12 267 param_schema: ParameterSchema,
70fbac84
DM
268 form: &str, // x-www-form-urlencoded body data
269 parts: &Parts,
270 uri_param: &HashMap<String, String, S>,
271) -> Result<Value, Error> {
70fbac84
DM
272 let mut param_list: Vec<(String, String)> = vec![];
273
274 if !form.is_empty() {
275 for (k, v) in form_urlencoded::parse(form.as_bytes()).into_owned() {
276 param_list.push((k, v));
277 }
278 }
279
280 if let Some(query_str) = parts.uri.query() {
281 for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
d43c407a
TL
282 if k == "_dc" {
283 continue;
284 } // skip extjs "disable cache" parameter
70fbac84
DM
285 param_list.push((k, v));
286 }
287 }
288
289 for (k, v) in uri_param {
290 param_list.push((k.clone(), v.clone()));
291 }
292
293 let params = parse_parameter_strings(&param_list, param_schema, true)?;
294
295 Ok(params)
296}
297
2bbd835b 298async fn get_request_parameters<S: 'static + BuildHasher + Send>(
b2362a12 299 param_schema: ParameterSchema,
9bc17e8d
DM
300 parts: Parts,
301 req_body: Body,
62ee2eb4 302 uri_param: HashMap<String, String, S>,
ad51d02a 303) -> Result<Value, Error> {
0ffbccce
DM
304 let mut is_json = false;
305
306 if let Some(value) = parts.headers.get(header::CONTENT_TYPE) {
8346f0d5
DM
307 match value.to_str().map(|v| v.split(';').next()) {
308 Ok(Some("application/x-www-form-urlencoded")) => {
309 is_json = false;
310 }
311 Ok(Some("application/json")) => {
312 is_json = true;
313 }
ad51d02a 314 _ => bail!("unsupported content type {:?}", value.to_str()),
0ffbccce
DM
315 }
316 }
317
eeff085d
TL
318 let body = TryStreamExt::map_err(req_body, |err| {
319 http_err!(BAD_REQUEST, "Problems reading request body: {}", err)
320 })
321 .try_fold(Vec::new(), |mut acc, chunk| async move {
322 // FIXME: max request body size?
323 if acc.len() + chunk.len() < 64 * 1024 {
324 acc.extend_from_slice(&*chunk);
325 Ok(acc)
326 } else {
327 Err(http_err!(BAD_REQUEST, "Request body too large"))
328 }
329 })
330 .await?;
9bc17e8d 331
d43c407a
TL
332 let utf8_data =
333 std::str::from_utf8(&body).map_err(|err| format_err!("Request body not uft8: {}", err))?;
0ffbccce 334
ad51d02a 335 if is_json {
70fbac84 336 let mut params: Value = serde_json::from_str(utf8_data)?;
ad51d02a 337 for (k, v) in uri_param {
75a5a689 338 if let Some((_optional, prop_schema)) = param_schema.lookup(&k) {
ad51d02a 339 params[&k] = parse_simple_value(&v, prop_schema)?;
9bc17e8d 340 }
ad51d02a 341 }
b2362a12 342 verify_json_object(&params, &param_schema)?;
ad51d02a 343 return Ok(params);
70fbac84
DM
344 } else {
345 parse_query_parameters(param_schema, utf8_data, &parts, &uri_param)
ad51d02a 346 }
9bc17e8d
DM
347}
348
7171b3e0
DM
349struct NoLogExtension();
350
ad51d02a 351async fn proxy_protected_request(
4b2cdeb9 352 info: &'static ApiMethod,
a3da38dd 353 mut parts: Parts,
f1204833 354 req_body: Body,
29633e2f 355 peer: &std::net::SocketAddr,
ad51d02a 356) -> Result<Response<Body>, Error> {
a3da38dd
DM
357 let mut uri_parts = parts.uri.clone().into_parts();
358
359 uri_parts.scheme = Some(http::uri::Scheme::HTTP);
360 uri_parts.authority = Some(http::uri::Authority::from_static("127.0.0.1:82"));
361 let new_uri = http::Uri::from_parts(uri_parts).unwrap();
362
363 parts.uri = new_uri;
364
29633e2f 365 let mut request = Request::from_parts(parts, req_body);
d43c407a
TL
366 request.headers_mut().insert(
367 header::FORWARDED,
368 format!("for=\"{}\";", peer).parse().unwrap(),
369 );
a3da38dd 370
ad51d02a
DM
371 let reload_timezone = info.reload_timezone;
372
a3da38dd
DM
373 let resp = hyper::client::Client::new()
374 .request(request)
fc7f0352 375 .map_err(Error::from)
91e45873 376 .map_ok(|mut resp| {
1cb99c23 377 resp.extensions_mut().insert(NoLogExtension());
7e03988c 378 resp
ad51d02a
DM
379 })
380 .await?;
a3da38dd 381
d43c407a
TL
382 if reload_timezone {
383 unsafe {
384 tzset();
385 }
386 }
1cb99c23 387
ad51d02a 388 Ok(resp)
f1204833
DM
389}
390
f7348a23 391pub(crate) async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher + Send>(
f757b30e 392 mut rpcenv: Env,
279ecfdf 393 info: &'static ApiMethod,
53daae8e 394 formatter: &'static dyn OutputFormatter,
9bc17e8d
DM
395 parts: Parts,
396 req_body: Body,
62ee2eb4 397 uri_param: HashMap<String, String, S>,
ad51d02a 398) -> Result<Response<Body>, Error> {
a154a8e8 399 let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
2f29f1c7 400 let compression = extract_compression_method(&parts.headers);
a154a8e8 401
70fbac84 402 let result = match info.handler {
329d40b5 403 ApiHandler::AsyncHttp(handler) => {
70fbac84
DM
404 let params = parse_query_parameters(info.parameters, "", &parts, &uri_param)?;
405 (handler)(parts, req_body, params, info, Box::new(rpcenv)).await
406 }
407 ApiHandler::Sync(handler) => {
d43c407a
TL
408 let params =
409 get_request_parameters(info.parameters, parts, req_body, uri_param).await?;
53daae8e 410 (handler)(params, info, &mut rpcenv).map(|data| formatter.format_data(data, &rpcenv))
70fbac84 411 }
bb084b9c 412 ApiHandler::Async(handler) => {
d43c407a
TL
413 let params =
414 get_request_parameters(info.parameters, parts, req_body, uri_param).await?;
bb084b9c
DM
415 (handler)(params, info, &mut rpcenv)
416 .await
53daae8e 417 .map(|data| formatter.format_data(data, &rpcenv))
bb084b9c 418 }
70fbac84 419 };
a154a8e8 420
2f29f1c7 421 let mut resp = match result {
70fbac84 422 Ok(resp) => resp,
ad51d02a
DM
423 Err(err) => {
424 if let Some(httperr) = err.downcast_ref::<HttpError>() {
425 if httperr.code == StatusCode::UNAUTHORIZED {
0a8d773a 426 tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await;
ad51d02a 427 }
4b2cdeb9 428 }
53daae8e 429 formatter.format_error(err)
ad51d02a
DM
430 }
431 };
4b2cdeb9 432
2f29f1c7
DC
433 let resp = match compression {
434 Some(CompressionMethod::Deflate) => {
435 resp.headers_mut().insert(
436 header::CONTENT_ENCODING,
437 CompressionMethod::Deflate.content_encoding(),
438 );
439 resp.map(|body| {
440 Body::wrap_stream(DeflateEncoder::with_quality(
f2f43e19 441 TryStreamExt::map_err(body, |err| {
2f29f1c7
DC
442 proxmox::io_format_err!("error during compression: {}", err)
443 }),
b84e8aae 444 Level::Default,
2f29f1c7
DC
445 ))
446 })
447 }
448 None => resp,
449 };
450
d43c407a
TL
451 if info.reload_timezone {
452 unsafe {
453 tzset();
454 }
455 }
ad51d02a 456
ad51d02a 457 Ok(resp)
7e21da6e
DM
458}
459
f4c514c1 460
826bb982 461fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
826bb982
DM
462 if let Some(ext) = filename.extension().and_then(|osstr| osstr.to_str()) {
463 return match ext {
464 "css" => ("text/css", false),
465 "html" => ("text/html", false),
466 "js" => ("application/javascript", false),
467 "json" => ("application/json", false),
468 "map" => ("application/json", false),
469 "png" => ("image/png", true),
470 "ico" => ("image/x-icon", true),
471 "gif" => ("image/gif", true),
472 "svg" => ("image/svg+xml", false),
473 "jar" => ("application/java-archive", true),
474 "woff" => ("application/font-woff", true),
475 "woff2" => ("application/font-woff2", true),
476 "ttf" => ("application/font-snft", true),
477 "pdf" => ("application/pdf", true),
478 "epub" => ("application/epub+zip", true),
479 "mp3" => ("audio/mpeg", true),
480 "oga" => ("audio/ogg", true),
481 "tgz" => ("application/x-compressed-tar", true),
482 _ => ("application/octet-stream", false),
483 };
484 }
485
486 ("application/octet-stream", false)
487}
488
59477ad2
DC
489async fn simple_static_file_download(
490 filename: PathBuf,
491 content_type: &'static str,
492 compression: Option<CompressionMethod>,
493) -> Result<Response<Body>, Error> {
91e45873 494 use tokio::io::AsyncReadExt;
9bc17e8d 495
91e45873
WB
496 let mut file = File::open(filename)
497 .await
8aa67ee7 498 .map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
9bc17e8d 499
91e45873 500 let mut data: Vec<u8> = Vec::new();
91e45873 501
59477ad2
DC
502 let mut response = match compression {
503 Some(CompressionMethod::Deflate) => {
b84e8aae 504 let mut enc = DeflateEncoder::with_quality(data, Level::Default);
2d485333
TL
505 enc.compress_vec(&mut file, CHUNK_SIZE_LIMIT as usize)
506 .await?;
59477ad2
DC
507 let mut response = Response::new(enc.into_inner().into());
508 response.headers_mut().insert(
509 header::CONTENT_ENCODING,
510 CompressionMethod::Deflate.content_encoding(),
511 );
512 response
513 }
514 None => {
515 file.read_to_end(&mut data)
516 .await
517 .map_err(|err| http_err!(BAD_REQUEST, "File read failed: {}", err))?;
518 Response::new(data.into())
519 }
520 };
521
91e45873
WB
522 response.headers_mut().insert(
523 header::CONTENT_TYPE,
d43c407a
TL
524 header::HeaderValue::from_static(content_type),
525 );
59477ad2 526
91e45873
WB
527 Ok(response)
528}
529
59477ad2
DC
530async fn chuncked_static_file_download(
531 filename: PathBuf,
532 content_type: &'static str,
533 compression: Option<CompressionMethod>,
534) -> Result<Response<Body>, Error> {
535 let mut resp = Response::builder()
536 .status(StatusCode::OK)
537 .header(header::CONTENT_TYPE, content_type);
826bb982 538
91e45873
WB
539 let file = File::open(filename)
540 .await
8aa67ee7 541 .map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
91e45873 542
59477ad2
DC
543 let body = match compression {
544 Some(CompressionMethod::Deflate) => {
545 resp = resp.header(
546 header::CONTENT_ENCODING,
547 CompressionMethod::Deflate.content_encoding(),
548 );
549 Body::wrap_stream(DeflateEncoder::with_quality(
550 AsyncReaderStream::new(file),
b84e8aae 551 Level::Default,
59477ad2
DC
552 ))
553 }
554 None => Body::wrap_stream(AsyncReaderStream::new(file)),
555 };
91e45873 556
59477ad2 557 Ok(resp.body(body).unwrap())
9bc17e8d
DM
558}
559
59477ad2
DC
560async fn handle_static_file_download(
561 filename: PathBuf,
562 compression: Option<CompressionMethod>,
563) -> Result<Response<Body>, Error> {
9c18e935 564 let metadata = tokio::fs::metadata(filename.clone())
8aa67ee7 565 .map_err(|err| http_err!(BAD_REQUEST, "File access problems: {}", err))
9c18e935
TL
566 .await?;
567
59477ad2
DC
568 let (content_type, nocomp) = extension_to_content_type(&filename);
569 let compression = if nocomp { None } else { compression };
570
571 if metadata.len() < CHUNK_SIZE_LIMIT {
572 simple_static_file_download(filename, content_type, compression).await
9c18e935 573 } else {
59477ad2 574 chuncked_static_file_download(filename, content_type, compression).await
9c18e935 575 }
9bc17e8d
DM
576}
577
c30816c1 578fn extract_lang_header(headers: &http::HeaderMap) -> Option<String> {
c47609fe 579 if let Some(Ok(cookie)) = headers.get("COOKIE").map(|v| v.to_str()) {
778c7d95 580 return extract_cookie(cookie, "PBSLangCookie");
c30816c1 581 }
c30816c1
FG
582 None
583}
584
4d84e869
DC
585// FIXME: support handling multiple compression methods
586fn extract_compression_method(headers: &http::HeaderMap) -> Option<CompressionMethod> {
c47609fe
TL
587 if let Some(Ok(encodings)) = headers.get(header::ACCEPT_ENCODING).map(|v| v.to_str()) {
588 for encoding in encodings.split(&[',', ' '][..]) {
589 if let Ok(method) = encoding.parse() {
590 return Some(method);
4d84e869
DC
591 }
592 }
593 }
4d84e869
DC
594 None
595}
596
29633e2f
TL
597async fn handle_request(
598 api: Arc<ApiConfig>,
599 req: Request<Body>,
600 peer: &std::net::SocketAddr,
601) -> Result<Response<Body>, Error> {
141de837 602 let (parts, body) = req.into_parts();
141de837 603 let method = parts.method.clone();
778c7d95 604 let (path, components) = normalize_uri_path(parts.uri.path())?;
141de837 605
9bc17e8d
DM
606 let comp_len = components.len();
607
4703ba81
TL
608 let query = parts.uri.query().unwrap_or_default();
609 if path.len() + query.len() > MAX_URI_QUERY_LENGTH {
610 return Ok(Response::builder()
611 .status(StatusCode::URI_TOO_LONG)
612 .body("".into())
613 .unwrap());
614 }
615
f1204833 616 let env_type = api.env_type();
36b7085e 617 let mut rpcenv = RestEnvironment::new(env_type, Arc::clone(&api));
e82dad97 618
29633e2f
TL
619 rpcenv.set_client_ip(Some(*peer));
620
26858dba 621 let auth = &api.api_auth;
4b40148c 622
b9903d63 623 let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
9989d2c4 624 let access_forbidden_time = std::time::Instant::now() + std::time::Duration::from_millis(500);
b9903d63 625
576e3bf2 626 if comp_len >= 1 && components[0] == "api2" {
9bc17e8d
DM
627 if comp_len >= 2 {
628 let format = components[1];
ad51d02a 629
53daae8e
DM
630 let formatter: &dyn OutputFormatter = match format {
631 "json" => JSON_FORMATTER,
632 "extjs" => EXTJS_FORMATTER,
d43c407a 633 _ => bail!("Unsupported output format '{}'.", format),
1571873d 634 };
9bc17e8d 635
e7ea17de 636 let mut uri_param = HashMap::new();
0ac61247 637 let api_method = api.find_method(&components[2..], method.clone(), &mut uri_param);
e7ea17de 638
0ac61247
TL
639 let mut auth_required = true;
640 if let Some(api_method) = api_method {
641 if let Permission::World = *api_method.access.permission {
642 auth_required = false; // no auth for endpoints with World permission
643 }
644 }
645
98b7d58b
DM
646 let mut user_info: Box<dyn UserInformation + Send + Sync> = Box::new(EmptyUserInformation {});
647
0ac61247 648 if auth_required {
fd6d2438 649 match auth.check_auth(&parts.headers, &method) {
98b7d58b
DM
650 Ok((authid, info)) => {
651 rpcenv.set_auth_id(Some(authid));
652 user_info = info;
653 }
26858dba
SR
654 Err(auth_err) => {
655 let err = match auth_err {
656 AuthError::Generic(err) => err,
657 AuthError::NoData => {
658 format_err!("no authentication credentials provided.")
659 }
660 };
36b7085e
DM
661 // fixme: log Username??
662 rpcenv.log_failed_auth(None, &err.to_string());
4fdf13f9 663
5ddf8cb1 664 // always delay unauthorized calls by 3 seconds (from start of request)
8aa67ee7 665 let err = http_err!(UNAUTHORIZED, "authentication failed - {}", err);
0a8d773a 666 tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await;
53daae8e 667 return Ok(formatter.format_error(err));
5ddf8cb1 668 }
b9903d63
DM
669 }
670 }
d7d23785 671
0ac61247 672 match api_method {
255f378a 673 None => {
8aa67ee7 674 let err = http_err!(NOT_FOUND, "Path '{}' not found.", path);
53daae8e 675 return Ok(formatter.format_error(err));
49d123ee 676 }
255f378a 677 Some(api_method) => {
e6dc35ac 678 let auth_id = rpcenv.get_auth_id();
98b7d58b 679 let user_info = user_info;
fd6d2438 680
d43c407a
TL
681 if !check_api_permission(
682 api_method.access.permission,
683 auth_id.as_deref(),
684 &uri_param,
685 user_info.as_ref(),
686 ) {
8aa67ee7 687 let err = http_err!(FORBIDDEN, "permission check failed");
0a8d773a 688 tokio::time::sleep_until(Instant::from_std(access_forbidden_time)).await;
53daae8e 689 return Ok(formatter.format_error(err));
4b40148c
DM
690 }
691
4299ca72 692 let result = if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
29633e2f 693 proxy_protected_request(api_method, parts, body, peer).await
f1204833 694 } else {
d43c407a
TL
695 handle_api_request(rpcenv, api_method, formatter, parts, body, uri_param)
696 .await
4299ca72
DM
697 };
698
8e7e2223
TL
699 let mut response = match result {
700 Ok(resp) => resp,
53daae8e 701 Err(err) => formatter.format_error(err),
8e7e2223
TL
702 };
703
e6dc35ac 704 if let Some(auth_id) = auth_id {
1b1a5537 705 response.extensions_mut().insert(AuthStringExtension(auth_id));
f1204833 706 }
8e7e2223
TL
707
708 return Ok(response);
7e21da6e 709 }
9bc17e8d
DM
710 }
711 }
d43c407a 712 } else {
7f168523 713 // not Auth required for accessing files!
9bc17e8d 714
7d4ef127 715 if method != hyper::Method::GET {
ad51d02a 716 bail!("Unsupported HTTP method {}", method);
7d4ef127
DM
717 }
718
f4c514c1 719 if comp_len == 0 {
c30816c1 720 let language = extract_lang_header(&parts.headers);
fd6d2438 721 match auth.check_auth(&parts.headers, &method) {
98b7d58b 722 Ok((auth_id, _user_info)) => {
7fa9a37c 723 return Ok(api.get_index(Some(auth_id), language, parts));
7f168523 724 }
26858dba
SR
725 Err(AuthError::Generic(_)) => {
726 tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await;
727 }
728 Err(AuthError::NoData) => {}
7f168523 729 }
7fa9a37c 730 return Ok(api.get_index(None, language, parts));
f4c514c1
DM
731 } else {
732 let filename = api.find_alias(&components);
59477ad2
DC
733 let compression = extract_compression_method(&parts.headers);
734 return handle_static_file_download(filename, compression).await;
f4c514c1 735 }
9bc17e8d
DM
736 }
737
8aa67ee7 738 Err(http_err!(NOT_FOUND, "Path '{}' not found.", path))
9bc17e8d 739}