]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/server/rest.rs
switch from failure to anyhow
[proxmox-backup.git] / src / server / rest.rs
index eb938c5363e079c5aed7fe2cd8bb28a72155885c..27ac93f25fa9e80d024cbe0180588630a90ffa54 100644 (file)
@@ -1,31 +1,36 @@
-use crate::tools;
-use crate::api::schema::*;
-use crate::api::router::*;
-use crate::api::config::*;
-use super::environment::RestEnvironment;
-use super::formatter::*;
-
-use std::fmt;
+use std::collections::HashMap;
+use std::future::Future;
+use std::hash::BuildHasher;
 use std::path::{Path, PathBuf};
+use std::pin::Pin;
 use std::sync::Arc;
-use std::collections::HashMap;
+use std::task::{Context, Poll};
 
-use failure::*;
+use anyhow::{bail, format_err, Error};
+use futures::future::{self, FutureExt, TryFutureExt};
+use futures::stream::TryStreamExt;
+use hyper::header;
+use hyper::http::request::Parts;
+use hyper::{Body, Request, Response, StatusCode};
 use serde_json::{json, Value};
+use tokio::fs::File;
+use tokio::time::Instant;
 use url::form_urlencoded;
 
-use futures::future::{self, Either};
-//use tokio::prelude::*;
-//use tokio::timer::Delay;
-use tokio::fs::File;
-//use bytes::{BytesMut, BufMut};
+use proxmox::http_err;
+use proxmox::api::{ApiHandler, ApiMethod, HttpError};
+use proxmox::api::{RpcEnvironment, RpcEnvironmentType, check_api_permission};
+use proxmox::api::schema::{ObjectSchema, parse_simple_value, verify_json_object, parse_parameter_strings};
 
-//use hyper::body::Payload;
-use hyper::http::request::Parts;
-use hyper::{Body, Request, Response, StatusCode};
-use hyper::service::{Service, NewService};
-use hyper::rt::{Future, Stream};
-use hyper::header;
+use super::environment::RestEnvironment;
+use super::formatter::*;
+use super::ApiConfig;
+
+use crate::auth_helpers::*;
+use crate::tools;
+use crate::config::cached_user_info::CachedUserInfo;
+
+extern "C"  { fn tzset(); }
 
 pub struct RestServer {
     pub api_config: Arc<ApiConfig>,
@@ -38,194 +43,280 @@ impl RestServer {
     }
 }
 
-impl NewService for RestServer
-{
-    type ReqBody = Body;
-    type ResBody = Body;
-    type Error = hyper::Error;
-    type InitError = hyper::Error;
-    type Service = ApiService;
-    type Future = Box<Future<Item = Self::Service, Error = Self::InitError> + Send>;
-    fn new_service(&self) -> Self::Future {
-        Box::new(future::ok(ApiService { api_config: self.api_config.clone() }))
+impl tower_service::Service<&tokio_openssl::SslStream<tokio::net::TcpStream>> for RestServer {
+    type Response = ApiService;
+    type Error = Error;
+    type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>;
+
+    fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn call(&mut self, ctx: &tokio_openssl::SslStream<tokio::net::TcpStream>) -> Self::Future {
+        match ctx.get_ref().peer_addr() {
+            Err(err) => {
+                future::err(format_err!("unable to get peer address - {}", err)).boxed()
+            }
+            Ok(peer) => {
+                future::ok(ApiService { peer, api_config: self.api_config.clone() }).boxed()
+            }
+        }
+    }
+}
+
+impl tower_service::Service<&tokio::net::TcpStream> for RestServer {
+    type Response = ApiService;
+    type Error = Error;
+    type Future = Pin<Box<dyn Future<Output = Result<ApiService, Error>> + Send>>;
+
+    fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn call(&mut self, ctx: &tokio::net::TcpStream) -> Self::Future {
+        match ctx.peer_addr() {
+            Err(err) => {
+                future::err(format_err!("unable to get peer address - {}", err)).boxed()
+            }
+            Ok(peer) => {
+                future::ok(ApiService { peer, api_config: self.api_config.clone() }).boxed()
+            }
+        }
     }
 }
 
 pub struct ApiService {
+    pub peer: std::net::SocketAddr,
     pub api_config: Arc<ApiConfig>,
 }
 
+fn log_response(
+    peer: &std::net::SocketAddr,
+    method: hyper::Method,
+    path: &str,
+    resp: &Response<Body>,
+) {
+
+    if resp.extensions().get::<NoLogExtension>().is_some() { return; };
+
+    let status = resp.status();
+
+    if !(status.is_success() || status.is_informational()) {
+        let reason = status.canonical_reason().unwrap_or("unknown reason");
+
+        let mut message = "request failed";
+        if let Some(data) = resp.extensions().get::<ErrorMessageExtension>() {
+            message = &data.0;
+        }
+
+        log::error!("{} {}: {} {}: [client {}] {}", method.as_str(), path, status.as_str(), reason, peer, message);
+    }
+}
+
+impl tower_service::Service<Request<Body>> for ApiService {
+    type Response = Response<Body>;
+    type Error = Error;
+    type Future = Pin<Box<dyn Future<Output = Result<Response<Body>, Self::Error>> + Send>>;
 
-impl Service for ApiService {
-    type ReqBody = Body;
-    type ResBody = Body;
-    type Error = hyper::Error;
-    type Future = Box<Future<Item = Response<Body>, Error = Self::Error> + Send>;
+    fn poll_ready(&mut self, _cx: &mut Context) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
 
-    fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
+    fn call(&mut self, req: Request<Body>) -> Self::Future {
+        let path = req.uri().path().to_owned();
+        let method = req.method().clone();
 
-        Box::new(handle_request(self.api_config.clone(), req).then(|result| {
-            match result {
-                Ok(res) => Ok::<_, hyper::Error>(res),
+        let peer = self.peer;
+        handle_request(self.api_config.clone(), req)
+            .map(move |result| match result {
+                Ok(res) => {
+                    log_response(&peer, method, &path, &res);
+                    Ok::<_, Self::Error>(res)
+                }
                 Err(err) => {
-                     if let Some(apierr) = err.downcast_ref::<HttpError>() {
+                    if let Some(apierr) = err.downcast_ref::<HttpError>() {
                         let mut resp = Response::new(Body::from(apierr.message.clone()));
                         *resp.status_mut() = apierr.code;
+                        log_response(&peer, method, &path, &resp);
                         Ok(resp)
                     } else {
                         let mut resp = Response::new(Body::from(err.to_string()));
                         *resp.status_mut() = StatusCode::BAD_REQUEST;
+                        log_response(&peer, method, &path, &resp);
                         Ok(resp)
                     }
                 }
-            }
-        }))
+            })
+            .boxed()
     }
 }
 
-#[derive(Debug, Fail)]
-pub struct HttpError {
-    pub code: StatusCode,
-    pub message: String,
-}
+fn parse_query_parameters<S: 'static + BuildHasher + Send>(
+    param_schema: &ObjectSchema,
+    form: &str, // x-www-form-urlencoded body data
+    parts: &Parts,
+    uri_param: &HashMap<String, String, S>,
+) -> Result<Value, Error> {
 
-impl HttpError {
-    pub fn new(code: StatusCode, message: String) -> Self {
-        HttpError { code, message }
+    let mut param_list: Vec<(String, String)> = vec![];
+
+    if !form.is_empty() {
+        for (k, v) in form_urlencoded::parse(form.as_bytes()).into_owned() {
+            param_list.push((k, v));
+        }
     }
-}
 
-impl fmt::Display for HttpError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "Error {}: {}", self.code, self.message)
+    if let Some(query_str) = parts.uri.query() {
+        for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
+            if k == "_dc" { continue; } // skip extjs "disable cache" parameter
+            param_list.push((k, v));
+        }
     }
-}
 
-macro_rules! http_err {
-    ($status:ident, $msg:expr) => {{
-        Error::from(HttpError::new(StatusCode::$status, $msg))
-    }}
+    for (k, v) in uri_param {
+        param_list.push((k.clone(), v.clone()));
+    }
+
+    let params = parse_parameter_strings(&param_list, param_schema, true)?;
+
+    Ok(params)
 }
 
-fn get_request_parameters_async(
-    info: &'static ApiMethod,
+async fn get_request_parameters<S: 'static + BuildHasher + Send>(
+    param_schema: &ObjectSchema,
     parts: Parts,
     req_body: Body,
-    uri_param: HashMap<String, String>,
-) -> Box<Future<Item = Value, Error = failure::Error> + Send>
-{
-    let resp = req_body
+    uri_param: HashMap<String, String, S>,
+) -> Result<Value, Error> {
+
+    let mut is_json = false;
+
+    if let Some(value) = parts.headers.get(header::CONTENT_TYPE) {
+        match value.to_str().map(|v| v.split(';').next()) {
+            Ok(Some("application/x-www-form-urlencoded")) => {
+                is_json = false;
+            }
+            Ok(Some("application/json")) => {
+                is_json = true;
+            }
+            _ => bail!("unsupported content type {:?}", value.to_str()),
+        }
+    }
+
+    let body = req_body
         .map_err(|err| http_err!(BAD_REQUEST, format!("Promlems reading request body: {}", err)))
-        .fold(Vec::new(), |mut acc, chunk| {
+        .try_fold(Vec::new(), |mut acc, chunk| async move {
             if acc.len() + chunk.len() < 64*1024 { //fimxe: max request body size?
                 acc.extend_from_slice(&*chunk);
                 Ok(acc)
+            } else {
+                Err(http_err!(BAD_REQUEST, "Request body too large".to_string()))
             }
-            else { Err(http_err!(BAD_REQUEST, format!("Request body too large"))) }
-        })
-        .and_then(move |body| {
+        }).await?;
 
-            let utf8 = std::str::from_utf8(&body)?;
+    let utf8_data = std::str::from_utf8(&body)
+        .map_err(|err| format_err!("Request body not uft8: {}", err))?;
 
-            println!("GOT BODY {:?}", utf8);
+    if is_json {
+        let mut params: Value = serde_json::from_str(utf8_data)?;
+        for (k, v) in uri_param {
+            if let Some((_optional, prop_schema)) = param_schema.lookup(&k) {
+                params[&k] = parse_simple_value(&v, prop_schema)?;
+            }
+        }
+        verify_json_object(&params, param_schema)?;
+        return Ok(params);
+    } else {
+        parse_query_parameters(param_schema, utf8_data, &parts, &uri_param)
+    }
+}
 
-            let mut param_list: Vec<(String, String)> = vec![];
+struct NoLogExtension();
 
-            if utf8.len() > 0 {
-                for (k, v) in form_urlencoded::parse(utf8.as_bytes()).into_owned() {
-                    param_list.push((k, v));
-                }
+async fn proxy_protected_request(
+    info: &'static ApiMethod,
+    mut parts: Parts,
+    req_body: Body,
+) -> Result<Response<Body>, Error> {
 
-            }
+    let mut uri_parts = parts.uri.clone().into_parts();
 
-            if let Some(query_str) = parts.uri.query() {
-                for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
-                    if k == "_dc" { continue; } // skip extjs "disable cache" parameter
-                    param_list.push((k, v));
-                }
-            }
+    uri_parts.scheme = Some(http::uri::Scheme::HTTP);
+    uri_parts.authority = Some(http::uri::Authority::from_static("127.0.0.1:82"));
+    let new_uri = http::Uri::from_parts(uri_parts).unwrap();
 
-            for (k, v) in uri_param {
-                param_list.push((k.clone(), v.clone()));
-            }
+    parts.uri = new_uri;
 
-            let params = parse_parameter_strings(&param_list, &info.parameters, true)?;
+    let request = Request::from_parts(parts, req_body);
 
-            println!("GOT PARAMS {}", params);
-            Ok(params)
-        });
+    let reload_timezone = info.reload_timezone;
 
-    Box::new(resp)
-}
+    let resp = hyper::client::Client::new()
+        .request(request)
+        .map_err(Error::from)
+        .map_ok(|mut resp| {
+            resp.extensions_mut().insert(NoLogExtension());
+            resp
+        })
+        .await?;
 
-fn handle_sync_api_request(
-    mut rpcenv: RestEnvironment,
-    info: &'static ApiMethod,
-    formatter: &'static OutputFormatter,
-    parts: Parts,
-    req_body: Body,
-    uri_param: HashMap<String, String>,
-) -> BoxFut
-{
-    let params = get_request_parameters_async(info, parts, req_body, uri_param);
-
-    let resp = params
-        .and_then(move |params| {
-            let resp = match (info.handler)(params, info, &mut rpcenv) {
-                Ok(data) => (formatter.format_result)(data, &rpcenv),
-                Err(err) =>  (formatter.format_error)(err),
-            };
-            Ok(resp)
-        });
+    if reload_timezone { unsafe { tzset(); } }
 
-    Box::new(resp)
+    Ok(resp)
 }
 
-fn handle_async_api_request(
-    mut rpcenv: RestEnvironment,
-    info: &'static ApiAsyncMethod,
+pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher + Send>(
+    mut rpcenv: Env,
+    info: &'static ApiMethod,
     formatter: &'static OutputFormatter,
     parts: Parts,
     req_body: Body,
-    uri_param: HashMap<String, String>,
-) -> BoxFut
-{
-    // fixme: convert parameters to Json
-    let mut param_list: Vec<(String, String)> = vec![];
+    uri_param: HashMap<String, String, S>,
+) -> Result<Response<Body>, Error> {
 
-    if let Some(query_str) = parts.uri.query() {
-        for (k, v) in form_urlencoded::parse(query_str.as_bytes()).into_owned() {
-            if k == "_dc" { continue; } // skip extjs "disable cache" parameter
-            param_list.push((k, v));
-        }
-    }
+    let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
 
-    for (k, v) in uri_param {
-        param_list.push((k.clone(), v.clone()));
-    }
-
-    let params = match parse_parameter_strings(&param_list, &info.parameters, true) {
-        Ok(v) => v,
-        Err(err) => {
-            let resp = (formatter.format_error)(Error::from(err));
-            return Box::new(future::ok(resp));
+    let result = match info.handler {
+        ApiHandler::AsyncHttp(handler) => {
+            let params = parse_query_parameters(info.parameters, "", &parts, &uri_param)?;
+            (handler)(parts, req_body, params, info, Box::new(rpcenv)).await
+        }
+        ApiHandler::Sync(handler) => {
+            let params = get_request_parameters(info.parameters, parts, req_body, uri_param).await?;
+            (handler)(params, info, &mut rpcenv)
+                .map(|data| (formatter.format_data)(data, &rpcenv))
+        }
+        ApiHandler::Async(handler) => {
+            let params = get_request_parameters(info.parameters, parts, req_body, uri_param).await?;
+            (handler)(params, info, &mut rpcenv)
+                .await
+                .map(|data| (formatter.format_data)(data, &rpcenv))
         }
     };
 
-    match (info.handler)(parts, req_body, params, info, &mut rpcenv) {
-        Ok(future) => future,
+    let resp = match result {
+        Ok(resp) => resp,
         Err(err) => {
-            let resp = (formatter.format_error)(Error::from(err));
-            Box::new(future::ok(resp))
+            if let Some(httperr) = err.downcast_ref::<HttpError>() {
+                if httperr.code == StatusCode::UNAUTHORIZED {
+                    tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await;
+                }
+            }
+            (formatter.format_error)(err)
         }
-    }
+    };
+
+    if info.reload_timezone { unsafe { tzset(); } }
+
+    Ok(resp)
 }
 
-fn get_index() ->  BoxFut {
+fn get_index(username: Option<String>, token: Option<String>) ->  Response<Body> {
+
+    let nodename = proxmox::tools::nodename();
+    let username = username.unwrap_or_else(|| String::from(""));
 
-    let nodename = tools::nodename();
-    let username = "fakelogin"; // todo: implement real auth
-    let token = "abc";
+    let token = token.unwrap_or_else(|| String::from(""));
 
     let setup = json!({
         "Setup": { "auth_cookie_name": "PBSAuthCookie" },
@@ -247,6 +338,7 @@ fn get_index() ->  BoxFut {
     <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
     <link rel="stylesheet" type="text/css" href="/extjs/crisp/resources/charts-all.css" />
     <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
+    <link rel="stylesheet" type="text/css" href="/css/ext6-pbs.css" />
     <script type='text/javascript'> function gettext(buf) {{ return buf; }} </script>
     <script type="text/javascript" src="/extjs/ext-all-debug.js"></script>
     <script type="text/javascript" src="/extjs/charts-debug.js"></script>
@@ -269,15 +361,11 @@ fn get_index() ->  BoxFut {
 </html>
 "###, setup.to_string());
 
-    let resp = Response::builder()
+    Response::builder()
         .status(StatusCode::OK)
         .header(header::CONTENT_TYPE, "text/html")
-        // emulate succssful login, so that Proxmox:Utils.authOk() returns true
-        .header(header::SET_COOKIE, "PBSAuthCookie=\"XXX\"") // fixme: remove
         .body(index.into())
-        .unwrap();
-
-    Box::new(future::ok(resp))
+        .unwrap()
 }
 
 fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
@@ -309,131 +397,219 @@ fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
     ("application/octet-stream", false)
 }
 
-fn simple_static_file_download(filename: PathBuf) ->  BoxFut {
+async fn simple_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> {
 
     let (content_type, _nocomp) = extension_to_content_type(&filename);
 
-    Box::new(File::open(filename)
-        .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
-        .and_then(move |file| {
-            let buf: Vec<u8> = Vec::new();
-            tokio::io::read_to_end(file, buf)
-                .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err)))
-                .and_then(move |data| {
-                    let mut response = Response::new(data.1.into());
-                    response.headers_mut().insert(
-                        header::CONTENT_TYPE,
-                        header::HeaderValue::from_static(content_type));
-                    Ok(response)
-                })
-        }))
-}
+    use tokio::io::AsyncReadExt;
 
-fn chuncked_static_file_download(filename: PathBuf) ->  BoxFut {
+    let mut file = File::open(filename)
+        .await
+        .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))?;
 
+    let mut data: Vec<u8> = Vec::new();
+    file.read_to_end(&mut data)
+        .await
+        .map_err(|err| http_err!(BAD_REQUEST, format!("File read failed: {}", err)))?;
+
+    let mut response = Response::new(data.into());
+    response.headers_mut().insert(
+        header::CONTENT_TYPE,
+        header::HeaderValue::from_static(content_type));
+    Ok(response)
+}
+
+async fn chuncked_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> {
     let (content_type, _nocomp) = extension_to_content_type(&filename);
 
-    Box::new(File::open(filename)
-        .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
-        .and_then(move |file| {
-            let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()).
-                map(|bytes| {
-                    //sigh - howto avoid copy here? or the whole map() ??
-                    hyper::Chunk::from(bytes.to_vec())
-                });
-            let body = Body::wrap_stream(payload);
-
-            // fixme: set other headers ?
-            Ok(Response::builder()
-               .status(StatusCode::OK)
-               .header(header::CONTENT_TYPE, content_type)
-               .body(body)
-               .unwrap())
-        }))
+    let file = File::open(filename)
+        .await
+        .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))?;
+
+    let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
+        .map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze()));
+    let body = Body::wrap_stream(payload);
+
+    // fixme: set other headers ?
+    Ok(Response::builder()
+       .status(StatusCode::OK)
+       .header(header::CONTENT_TYPE, content_type)
+       .body(body)
+       .unwrap()
+    )
 }
 
-fn handle_static_file_download(filename: PathBuf) ->  BoxFut {
+async fn handle_static_file_download(filename: PathBuf) ->  Result<Response<Body>, Error> {
 
-    let response = tokio::fs::metadata(filename.clone())
+    let metadata = tokio::fs::metadata(filename.clone())
         .map_err(|err| http_err!(BAD_REQUEST, format!("File access problems: {}", err)))
-        .and_then(|metadata| {
-            if metadata.len() < 1024*32 {
-                Either::A(simple_static_file_download(filename))
-            } else {
-                Either::B(chuncked_static_file_download(filename))
-             }
-        });
+        .await?;
 
-    return Box::new(response);
+    if metadata.len() < 1024*32 {
+        simple_static_file_download(filename).await
+    } else {
+        chuncked_static_file_download(filename).await
+    }
 }
 
-pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> BoxFut {
+fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>) {
 
-    let (parts, body) = req.into_parts();
+    let mut ticket = None;
+    if let Some(raw_cookie) = headers.get("COOKIE") {
+        if let Ok(cookie) = raw_cookie.to_str() {
+            ticket = tools::extract_auth_cookie(cookie, "PBSAuthCookie");
+        }
+    }
 
-    let method = parts.method.clone();
-    let path = parts.uri.path();
+    let token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) {
+        Some(Ok(v)) => Some(v.to_owned()),
+        _ => None,
+    };
+
+    (ticket, token)
+}
 
-    // normalize path
-    // do not allow ".", "..", or hidden files ".XXXX"
-    // also remove empty path components
+fn check_auth(
+    method: &hyper::Method,
+    ticket: &Option<String>,
+    token: &Option<String>,
+    user_info: &CachedUserInfo,
+) -> Result<String, Error> {
 
-    let items = path.split('/');
-    let mut path = String::new();
-    let mut components = vec![];
+    let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
 
-    for name in items {
-        if name.is_empty() { continue; }
-        if name.starts_with(".") {
-            return Box::new(future::err(http_err!(BAD_REQUEST, "Path contains illegal components.".to_string())));
+    let username = match ticket {
+        Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) {
+            Ok((_age, Some(username))) => username.to_owned(),
+            Ok((_, None)) => bail!("ticket without username."),
+            Err(err) => return Err(err),
         }
-        path.push('/');
-        path.push_str(name);
-        components.push(name);
+        None => bail!("missing ticket"),
+    };
+
+    if !user_info.is_active_user(&username) {
+        bail!("user account disabled or expired.");
     }
 
+    if method != hyper::Method::GET {
+        if let Some(token) = token {
+            println!("CSRF prevention token: {:?}", token);
+            verify_csrf_prevention_token(csrf_secret(), &username, &token, -300, ticket_lifetime)?;
+        } else {
+            bail!("missing CSRF prevention token");
+        }
+    }
+
+    Ok(username)
+}
+
+pub async fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> Result<Response<Body>, Error> {
+
+    let (parts, body) = req.into_parts();
+
+    let method = parts.method.clone();
+    let (path, components) = tools::normalize_uri_path(parts.uri.path())?;
+
     let comp_len = components.len();
 
     println!("REQUEST {} {}", method, path);
     println!("COMPO {:?}", components);
 
-    let rpcenv = RestEnvironment::new(RpcEnvironmentType::PRIVILEDGED);
+    let env_type = api.env_type();
+    let mut rpcenv = RestEnvironment::new(env_type);
+
+    let user_info = CachedUserInfo::new()?;
+
+    let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
+    let access_forbidden_time = std::time::Instant::now() + std::time::Duration::from_millis(500);
 
     if comp_len >= 1 && components[0] == "api2" {
-        println!("GOT API REQUEST");
+
         if comp_len >= 2 {
+
             let format = components[1];
+
             let formatter = match format {
                 "json" => &JSON_FORMATTER,
                 "extjs" => &EXTJS_FORMATTER,
-                _ =>  {
-                    return Box::new(future::err(http_err!(BAD_REQUEST, format!("Unsupported output format '{}'.", format))));
-                }
+                _ =>  bail!("Unsupported output format '{}'.", format),
             };
 
             let mut uri_param = HashMap::new();
 
-            // fixme: handle auth
+            if comp_len == 4 && components[2] == "access" && (
+                (components[3] == "ticket" && method ==  hyper::Method::POST) ||
+                (components[3] == "domains" && method ==  hyper::Method::GET)
+            ) {
+                // explicitly allow those calls without auth
+            } else {
+                let (ticket, token) = extract_auth_data(&parts.headers);
+                match check_auth(&method, &ticket, &token, &user_info) {
+                    Ok(username) => rpcenv.set_user(Some(username)),
+                    Err(err) => {
+                        // always delay unauthorized calls by 3 seconds (from start of request)
+                        let err = http_err!(UNAUTHORIZED, format!("authentication failed - {}", err));
+                        tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await;
+                        return Ok((formatter.format_error)(err));
+                    }
+                }
+            }
+
             match api.find_method(&components[2..], method, &mut uri_param) {
-                MethodDefinition::None => {}
-                MethodDefinition::Simple(api_method) => {
-                    return handle_sync_api_request(rpcenv, api_method, formatter, parts, body, uri_param);
+                None => {
+                    let err = http_err!(NOT_FOUND, "Path not found.".to_string());
+                    return Ok((formatter.format_error)(err));
                 }
-                MethodDefinition::Async(async_method) => {
-                    return handle_async_api_request(rpcenv, async_method, formatter, parts, body, uri_param);
+                Some(api_method) => {
+                    let user = rpcenv.get_user();
+                    if !check_api_permission(api_method.access.permission, user.as_deref(), &uri_param, &user_info) {
+                        let err = http_err!(FORBIDDEN, format!("permission check failed"));
+                        tokio::time::delay_until(Instant::from_std(access_forbidden_time)).await;
+                        return Ok((formatter.format_error)(err));
+                    }
+
+                    let result = if api_method.protected && env_type == RpcEnvironmentType::PUBLIC {
+                        proxy_protected_request(api_method, parts, body).await
+                    } else {
+                        handle_api_request(rpcenv, api_method, formatter, parts, body, uri_param).await
+                    };
+
+                    if let Err(err) = result {
+                        return Ok((formatter.format_error)(err));
+                    }
+                    return result;
                 }
             }
+
+        }
+     } else {
+        // not Auth required for accessing files!
+
+        if method != hyper::Method::GET {
+            bail!("Unsupported HTTP method {}", method);
         }
-    } else {
-        // not Auth for accessing files!
 
         if comp_len == 0 {
-            return get_index();
+            let (ticket, token) = extract_auth_data(&parts.headers);
+            if ticket != None {
+                match check_auth(&method, &ticket, &token, &user_info) {
+                    Ok(username) => {
+                        let new_token = assemble_csrf_prevention_token(csrf_secret(), &username);
+                        return Ok(get_index(Some(username), Some(new_token)));
+                    }
+                    _ => {
+                        tokio::time::delay_until(Instant::from_std(delay_unauth_time)).await;
+                        return Ok(get_index(None, None));
+                    }
+                }
+            } else {
+                return Ok(get_index(None, None));
+            }
         } else {
             let filename = api.find_alias(&components);
-            return handle_static_file_download(filename);
+            return handle_static_file_download(filename).await;
         }
     }
 
-    Box::new(future::err(http_err!(NOT_FOUND, "Path not found.".to_string())))
+    Err(http_err!(NOT_FOUND, "Path not found.".to_string()))
 }