skyhook 0.5.51

Application server for Ordinary
Documentation
use std::sync::Arc;

use axum::body::Bytes;
use axum::extract::{MatchedPath, Path, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect};
use axum::{Form, Json};
use axum_extra::extract::CookieJar;
use flexbuffers::{BuilderOptions, VectorBuilder};
use ordinary_config::ActionTrigger;
use ordinary_types::{
    Kind, flexbuffer_reader_to_json, flexbuffer_reader_to_path_component, json_to_flexbuffer_vec,
};

use crate::server::OrdinaryAppServerState;
use base64::{Engine as B64Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use hyper::header::CONTENT_TYPE;
use ordinary_action::Action;

pub async fn invoke(
    State(state): State<Arc<OrdinaryAppServerState>>,
    Path(idx): Path<u8>,
    headers: HeaderMap,
    body: Bytes,
) -> impl IntoResponse {
    // todo: check origin for valid template.actions routes

    let span = tracing::info_span!(
        "action",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
        tgr = "Ordinary",
    );

    span.in_scope(|| {
        if let Ok(root) = flexbuffers::Reader::get_root(body.as_ref())
            && let Some(action) = state.actions.get(idx as usize)
        {
            span.record("i", action.config.idx);
            span.record("nm", tracing::field::display(&action.config.name));

            let mut builder = flexbuffers::Builder::new(&BuilderOptions::SHARE_NONE);
            let mut payload_vec = builder.start_vector();

            if let Err(err) =
                action
                    .config
                    .accepts
                    .copy_to(&root.as_vector().idx(0), &mut payload_vec, None)
            {
                tracing::error!(%err);
                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
            }

            if let Some(value) = check_auth_header(&state, &headers, action, &mut payload_vec) {
                return value.into_response();
            }

            payload_vec.end_vector();

            if let Ok(res) = action.call(builder.view(), &state.actions) {
                return (StatusCode::OK, res).into_response();
            }
        }

        StatusCode::NOT_FOUND.into_response()
    })
}

#[allow(clippy::too_many_lines)]
pub async fn form(
    State(state): State<Arc<OrdinaryAppServerState>>,
    path: MatchedPath,
    jar: CookieJar,
    body: Form<serde_json::Value>,
) -> Result<Redirect, StatusCode> {
    // todo: check origin for valid template.actions routes

    let span = tracing::info_span!(
        "action",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
        tgr = "Form",
    );

    span.in_scope(|| {
        if let Some(idx) = state.action_route_map.get(path.as_str())
            && let Some(action) = state.actions.get(*idx)
        {
            span.record("i", action.config.idx);
            span.record("nm", tracing::field::display(&action.config.name));

            let mut builder = flexbuffers::Builder::new(&BuilderOptions::SHARE_NONE);
            let mut payload_vec = builder.start_vector();

            if let Err(err) =
                json_to_flexbuffer_vec(&action.config.accepts, &body, &mut payload_vec)
            {
                tracing::error!("{err}");
                return Err(StatusCode::INTERNAL_SERVER_ERROR);
            }

            if let Some(_check) = &action.config.protected {
                let cookie_name = if state.secure_cookies {
                    "__Host-ORDINARY-ACCESS-TOKEN"
                } else {
                    "ORDINARY-ACCESS-TOKEN"
                };

                let token = if let Some(token) = jar.get(cookie_name) {
                    if let Ok(token) = b64.decode(token.value()) {
                        token
                    } else {
                        return Err(StatusCode::UNAUTHORIZED);
                    }
                } else {
                    return Err(StatusCode::UNAUTHORIZED);
                };

                match state.auth.verify_access_token(&token) {
                    Ok((_account, claims_root)) => {
                        let mut claims_vec = payload_vec.start_vector();

                        let empty_system_claims = claims_vec.start_vector();
                        empty_system_claims.end_vector();

                        for field in &state.auth.config.access_token.claims {
                            // todo: use the check to validate any claims

                            if let Err(err) = field.kind.copy_to(
                                &claims_root.idx(field.idx as usize),
                                &mut claims_vec,
                                None,
                            ) {
                                tracing::error!(%err);
                                return Err(StatusCode::INTERNAL_SERVER_ERROR);
                            }
                        }

                        claims_vec.end_vector();
                    }
                    Err(err) => {
                        tracing::error!("{err}");
                        return Err(StatusCode::UNAUTHORIZED);
                    }
                }
            }

            payload_vec.end_vector();

            match action.call(builder.view(), &state.actions) {
                Ok(res) => match flexbuffers::Reader::get_root(&res[..]) {
                    Ok(root) => {
                        for trigger in &action.config.triggered_by {
                            if let ActionTrigger::Form {
                                route: _,
                                method: _,
                                redirect,
                            } = trigger
                            {
                                let mut redirect = redirect.clone();

                                if let Ok(payload_root) =
                                    flexbuffers::Reader::get_root(builder.view())
                                {
                                    if redirect.contains("{accepts}") {
                                        redirect = redirect.replace(
                                            "{accepts}",
                                            &match flexbuffer_reader_to_path_component(
                                                &action.config.returns,
                                                &payload_root.as_vector().idx(0),
                                            ) {
                                                Ok(v) => v,
                                                Err(err) => {
                                                    tracing::error!(%err);
                                                    return Err(StatusCode::INTERNAL_SERVER_ERROR);
                                                }
                                            },
                                        );
                                    }

                                    if redirect.contains("{accepts.")
                                        && let Kind::Object { name: _, fields } =
                                            &action.config.accepts
                                    {
                                        let accepts_vec =
                                            payload_root.as_vector().idx(0).as_vector();

                                        for field in fields {
                                            redirect = redirect.replace(
                                                &format!("{{accepts.{}}}", field.name),
                                                &match flexbuffer_reader_to_path_component(
                                                    &field.kind,
                                                    &accepts_vec.idx(field.idx as usize),
                                                ) {
                                                    Ok(v) => v,
                                                    Err(err) => {
                                                        tracing::error!(%err);
                                                        return Err(
                                                            StatusCode::INTERNAL_SERVER_ERROR,
                                                        );
                                                    }
                                                },
                                            );
                                        }
                                    }

                                    // todo: handle `accepts` lists
                                }

                                if redirect.contains("{returns}") {
                                    redirect = redirect.replace(
                                        "{returns}",
                                        &match flexbuffer_reader_to_path_component(
                                            &action.config.returns,
                                            &root,
                                        ) {
                                            Ok(v) => v,
                                            Err(err) => {
                                                tracing::error!(%err);
                                                return Err(StatusCode::INTERNAL_SERVER_ERROR);
                                            }
                                        },
                                    );
                                }

                                if redirect.contains("{returns.")
                                    && let Kind::Object { name: _, fields } = &action.config.returns
                                {
                                    let root_vec = root.as_vector();

                                    for field in fields {
                                        redirect = redirect.replace(
                                            &format!("{{returns.{}}}", field.name),
                                            &match flexbuffer_reader_to_path_component(
                                                &field.kind,
                                                &root_vec.idx(field.idx as usize),
                                            ) {
                                                Ok(v) => v,
                                                Err(err) => {
                                                    tracing::error!(%err);
                                                    return Err(StatusCode::INTERNAL_SERVER_ERROR);
                                                }
                                            },
                                        );
                                    }
                                }

                                // todo: handle `returns` lists

                                return Ok(Redirect::to(&redirect));
                            }
                        }
                    }
                    Err(err) => tracing::error!("{err}"),
                },
                Err(err) => {
                    tracing::error!("{err}");
                }
            }
        }

        Err(StatusCode::INTERNAL_SERVER_ERROR)
    })
}

#[allow(clippy::too_many_lines)]
pub async fn json(
    State(state): State<Arc<OrdinaryAppServerState>>,
    path: MatchedPath,
    headers: HeaderMap,
    Json(json_body): Json<serde_json::Value>,
) -> impl IntoResponse {
    // todo: check origin for valid template.actions routes

    let span = tracing::info_span!(
        "action",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
        tgr = "Json",
    );

    span.in_scope(|| {
        if let Some(idx) = state.action_route_map.get(path.as_str()) {
            return if let Some(action) = state.actions.get(*idx) {
                span.record("i", action.config.idx);
                span.record("nm", tracing::field::display(&action.config.name));

                let mut builder = flexbuffers::Builder::new(&BuilderOptions::SHARE_NONE);
                let mut payload_vec = builder.start_vector();

                if let Err(err) =
                    json_to_flexbuffer_vec(&action.config.accepts, &json_body, &mut payload_vec)
                {
                    tracing::error!("{err}");
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }

                if let Some(value) = check_auth_header(&state, &headers, action, &mut payload_vec) {
                    return value.into_response();
                }

                payload_vec.end_vector();

                match action.call(builder.view(), &state.actions) {
                    Ok(res) => match flexbuffers::Reader::get_root(&res[..]) {
                        Ok(root) => {
                            let json_res =
                                match flexbuffer_reader_to_json(&action.config.returns, &root) {
                                    Ok(res) => res,
                                    Err(err) => {
                                        tracing::error!(%err);
                                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                                    }
                                };

                            (
                                StatusCode::OK,
                                [(CONTENT_TYPE, "application/json")],
                                json_res.to_string(),
                            )
                                .into_response()
                        }
                        Err(err) => {
                            tracing::error!("{err}");

                            StatusCode::INTERNAL_SERVER_ERROR.into_response()
                        }
                    },
                    Err(err) => {
                        tracing::error!("{err}");

                        StatusCode::INTERNAL_SERVER_ERROR.into_response()
                    }
                }
            } else {
                tracing::error!("no action at for idx {}", idx);
                StatusCode::NOT_FOUND.into_response()
            };
        }

        tracing::error!("no action at path {}", path.as_str());
        StatusCode::NOT_FOUND.into_response()
    })
}

fn check_auth_header(
    state: &Arc<OrdinaryAppServerState>,
    headers: &HeaderMap,
    action: &Action,
    payload_vec: &mut VectorBuilder,
) -> Option<StatusCode> {
    if let Some(_check) = &action.config.protected {
        if let Some(val) = headers.get("authorization") {
            if let Ok(str_val) = val.to_str() {
                if let Some(b64_token) = str_val.strip_prefix("Bearer ") {
                    if let Ok(token) = b64.decode(b64_token) {
                        match state.auth.verify_access_token(&token) {
                            Ok((_account, claims_root)) => {
                                let mut claims_vec = payload_vec.start_vector();

                                let empty_system_claims = claims_vec.start_vector();
                                empty_system_claims.end_vector();

                                for field in &state.auth.config.access_token.claims {
                                    // todo: use the check to validate any claims

                                    if let Err(err) = field.kind.copy_to(
                                        &claims_root.idx(field.idx as usize),
                                        &mut claims_vec,
                                        None,
                                    ) {
                                        tracing::error!(%err);
                                        return Some(StatusCode::INTERNAL_SERVER_ERROR);
                                    }
                                }

                                claims_vec.end_vector();
                            }
                            Err(err) => {
                                tracing::error!("{err}");
                                return Some(StatusCode::UNAUTHORIZED);
                            }
                        }
                    } else {
                        return Some(StatusCode::UNAUTHORIZED);
                    }
                } else {
                    return Some(StatusCode::UNAUTHORIZED);
                }
            } else {
                return Some(StatusCode::UNAUTHORIZED);
            }
        } else {
            return Some(StatusCode::UNAUTHORIZED);
        }
    }

    None
}