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