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