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