kvarn-reference 0.4.0

An implementation of the Kvarn web server framework.
use comprash::ClientCachePreference;
use internals::mime;
use kvarn::prelude::*;

/// Bullshittery with some futures not being Sync.
///
/// Use with care.
///
/// It should not break anything, as we are guaranteed (by the runtime) to only run a
/// task on one thread.
///
/// # Examples
///
/// ```no_compile
/// // Notice the binding to a variable and then awaiting it.
/// let future = UnsafeSendSyncFuture::new(resolver.ipv4_lookup(query));
/// let result = future.await;
/// ```
struct UnsafeSendSyncFuture<F>(F);
impl<F> UnsafeSendSyncFuture<F> {
    fn new(future: F) -> UnsafeSendSyncFuture<Pin<Box<F>>> {
        UnsafeSendSyncFuture(Box::pin(future))
    }
}
// That's the point!
#[allow(clippy::non_send_fields_in_send_ty)]
unsafe impl<F> Send for UnsafeSendSyncFuture<F> {}
unsafe impl<F> Sync for UnsafeSendSyncFuture<F> {}
impl<O, F: Future<Output = O> + Unpin> Future for UnsafeSendSyncFuture<F> {
    type Output = O;
    fn poll(mut self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
        Future::poll(Pin::new(&mut self.0), ctx)
    }
}

pub fn icelk_extensions() -> Extensions {
    // Mount all extensions to server
    let mut extensions = kvarn_extensions::new();

    let resolver_opts = trust_dns_resolver::config::ResolverOpts {
        cache_size: 0,
        validate: false,
        timeout: Duration::from_millis(1000),
        ..Default::default()
    };
    let mut resolver_config = trust_dns_resolver::config::ResolverConfig::new();
    resolver_config.add_name_server(trust_dns_resolver::config::NameServerConfig {
        socket_addr: SocketAddr::V4(net::SocketAddrV4::new(net::Ipv4Addr::LOCALHOST, 53)),
        protocol: trust_dns_resolver::config::Protocol::Udp,
        tls_dns_name: None,
        trust_nx_responses: true,
        tls_config: None,
    });
    let resolver = trust_dns_resolver::AsyncResolver::tokio(resolver_config, resolver_opts)
        .expect("Failed to create a resolver");

    extensions.add_prepare_single(
        "/dns/lookup",
        prepare!(
            req,
            host,
            _path,
            _addr,
            move |resolver: trust_dns_resolver::TokioAsyncResolver| {
                let queries = utils::parse::query(req.uri().query().unwrap_or(""));
                let body = if let Some(domain) = queries.get("domain") {
                    let mut body = Arc::new(Mutex::new(BytesMut::with_capacity(64)));

                    macro_rules! append_body {
                        ($result: expr, $kind: expr, $mod_name: ident, $modification: expr) => {{
                            let body = Arc::clone(&body);
                            let future = async move {
                                let future = UnsafeSendSyncFuture::new($result);
                                if let Ok(lookup) = future.await {
                                    let mut lock = body.lock().await;
                                    for $mod_name in lookup.iter() {
                                        let record = $modification;
                                        lock.extend_from_slice(
                                            format!("{} {}\n", $kind, record).as_bytes(),
                                        );
                                    }
                                }
                            };
                            future
                        }};
                        ($result: expr, $kind: expr) => {{
                            append_body!($result, $kind, v, v)
                        }};
                    }

                    let a = append_body!(resolver.ipv4_lookup(domain.value()), "A");
                    let aaaa = append_body!(resolver.ipv6_lookup(domain.value()), "AAAA");
                    let cname = append_body!(
                        resolver.lookup(
                            domain.value(),
                            trust_dns_resolver::proto::rr::RecordType::CNAME,
                            trust_dns_resolver::proto::xfer::DnsRequestOptions::default()
                        ),
                        "CNAME"
                    );
                    let mx =
                        append_body!(resolver.mx_lookup(domain.value()), "MX", mx, mx.exchange());
                    let txt = append_body!(resolver.txt_lookup(domain.value()), "TXT");

                    futures_util::join!(a, aaaa, cname, mx, txt);

                    let body = std::mem::take(Arc::get_mut(&mut body).unwrap());
                    body.into_inner().freeze()
                } else {
                    return default_error_response(
                        StatusCode::BAD_REQUEST,
                        host,
                        Some("there must be a `domain` key-value pair in the query"),
                    )
                    .await;
                };

                if body.is_empty() {
                    return default_error_response(
                        StatusCode::NOT_FOUND,
                        host,
                        Some("no DNS entry was found"),
                    )
                    .await;
                }

                FatResponse::no_cache(Response::new(body))
                    .with_compress(comprash::CompressPreference::None)
                    .with_content_type(&mime::TEXT_PLAIN)
            }
        ),
    );

    extensions.add_prepare_single(
        "/dns/check-dns-over-tls",
        prepare!(req, host, _, _, {
                let queries = utils::parse::query(req.uri().query().unwrap_or(""));

                let result = if let (Some(ip), Some(name)) = (queries.get("ip"), queries.get("name")) {
                    let ip = if let Ok(ip) = ip.value().parse() {
                        ip
                    } else {
                        return default_error_response(StatusCode::BAD_REQUEST, host, Some("the value isn't a valid IP address")).await;
                    };

                    let resolver_config = trust_dns_resolver::config::ResolverConfig::from_parts(
                        None,
                        vec![],
                        trust_dns_resolver::config::NameServerConfigGroup::from_ips_tls(
                            &[ip],
                            853,
                            name.value().into(),
                            false,
                        ),
                    );
                    if let Ok(resolver) = trust_dns_resolver::AsyncResolver::tokio(
                        resolver_config,
                        trust_dns_resolver::config::ResolverOpts {
                            timeout: Duration::from_secs_f64(2.0),
                            validate: false,
                            ..Default::default()
                        }
                    ) {
                        let query = queries.get("lookup-name").map(utils::parse::QueryPair::value).unwrap_or("icelk.dev.");
                        let future = UnsafeSendSyncFuture::new(resolver.ipv4_lookup(query));
                        let result = future.await;
                        if result.is_ok() {
                            "supported"
                        } else {
                            "unsupported"
                        }
                    } else {
                        return default_error_response(StatusCode::INTERNAL_SERVER_ERROR, host, Some("Creation of resolver failed.")).await;
                    }
                } else {
                    return default_error_response(
                        StatusCode::BAD_REQUEST,
                        host,
                        Some("there must be a `ip` key with a IP address as the value and a `name` with the corresponding host name as the value.\
                             It can have a `query-name` to specify which host name to test the look up with.")
                    )
                    .await;
                };

                FatResponse::no_cache(Response::new(result.into()))
                    .with_compress(comprash::CompressPreference::None)
                    .with_content_type(&mime::TEXT_PLAIN)
        }),
    );
    extensions.add_prepare_single(
        "/ip",
        prepare!(_req, _, _, addr, {
            FatResponse::no_cache(Response::new(addr.ip().to_string().into()))
                .with_compress(comprash::CompressPreference::None)
                .with_content_type(&mime::TEXT_PLAIN)
        }),
    );

    kvarn_extensions::force_cache(
        &mut extensions,
        &[
            (".png", ClientCachePreference::Changing),
            (".ico", ClientCachePreference::Full),
            (".woff2", ClientCachePreference::Full),
            ("/highlight.js/", ClientCachePreference::Full),
        ],
    );

    let base_csp = CspRule::default().img_src(CspValueSet::default().uri("https://kvarn.org"));
    extensions.with_csp(
        Csp::empty()
            .add("*", base_csp.clone())
            .add(
                "/index.html",
                base_csp.script_src(CspValueSet::default().unsafe_inline()),
            )
            .add("/api/*", CspRule::empty())
            .add("/ip", CspRule::empty())
            .arc(),
    );

    extensions
}
pub async fn icelk(extensions: Extensions) -> (Host, kvarn_search::SearchEngineHandle) {
    let mut host = host_from_name("icelk.dev", "../icelk.dev/", extensions);
    host.disable_client_cache().disable_server_cache();

    let se_options = kvarn_search::Options::default();
    let se_handle = kvarn_search::mount_search(&mut host.extensions, "/search", se_options).await;
    se_handle.index_all(&host).await;

    (host, se_handle)
}

pub fn icelk_doc_extensions() -> Extensions {
    let mut extensions = Extensions::new();

    extensions.add_present_internal("tmpl", Box::new(kvarn_extensions::templates_ext));

    kvarn_extensions::force_cache(
        &mut extensions,
        &[
            (".html", ClientCachePreference::None),
            (".woff2", ClientCachePreference::Full),
            (".woff", ClientCachePreference::Full),
            (".svg", ClientCachePreference::Changing),
            (".js", ClientCachePreference::Changing),
            (".css", ClientCachePreference::Changing),
        ],
    );

    extensions.with_csp(
        Csp::empty()
            .add(
                "*",
                CspRule::default().img_src(CspValueSet::default().uri("https://kvarn.org")),
            )
            .arc(),
    );
    extensions
}
pub fn icelk_doc(extensions: Extensions) -> Host {
    let mut host = host_from_name("doc.icelk.dev", "../icelk.dev/doc/", extensions);

    host.disable_server_cache().disable_client_cache();

    host
}

pub fn kvarn_extensions() -> Extensions {
    let mut extensions = kvarn_extensions::new();
    kvarn_extensions::force_cache(
        &mut extensions,
        &[
            (".png", ClientCachePreference::Changing),
            (".woff2", ClientCachePreference::Full),
            (".woff", ClientCachePreference::Full),
            (".svg", ClientCachePreference::Changing),
            ("/highlight.js/", ClientCachePreference::Full),
        ],
    );

    let kvarn_cors = Cors::empty()
        .add(
            "/logo.svg",
            CorsAllowList::new(Duration::from_secs(60 * 60 * 24 * 14))
                .add_origin("https://github.com")
                .add_origin("https://doc.kvarn.org"),
        )
        .add(
            "/favicon.svg",
            CorsAllowList::new(Duration::from_secs(60 * 60 * 24 * 14))
                .add_origin("https://doc.kvarn.org"),
        )
        .arc();
    extensions.with_cors(kvarn_cors);
    extensions
}
pub fn kvarn(extensions: Extensions) -> Host {
    let mut host = host_from_name("kvarn.org", "../kvarn.org/", extensions);

    host.disable_client_cache().disable_server_cache();

    host
}

pub fn kvarn_doc_extensions() -> Extensions {
    let mut extensions = Extensions::new();

    kvarn_extensions::force_cache(
        &mut extensions,
        &[
            (".html", ClientCachePreference::None),
            (".woff2", ClientCachePreference::Full),
            (".woff", ClientCachePreference::Full),
            (".svg", ClientCachePreference::Changing),
            (".js", ClientCachePreference::Changing),
            (".css", ClientCachePreference::Changing),
        ],
    );

    extensions.add_prepare_single(
        "/index.html",
        prepare!(_, _, _, _, {
            let response = Response::builder()
                .status(StatusCode::PERMANENT_REDIRECT)
                .header("location", "kvarn/")
                .body(Bytes::new())
                .expect("we know this is ok.");
            FatResponse::cache(response)
        }),
    );

    extensions.with_csp(
        Csp::empty()
            .add(
                "*",
                CspRule::default().img_src(CspValueSet::default().uri("https://kvarn.org")),
            )
            .arc(),
    );
    extensions
}
pub fn kvarn_doc(extensions: Extensions) -> Host {
    let mut host = host_from_name("doc.kvarn.org", "../kvarn/target/doc/", extensions);

    host.options.set_public_data_dir(".");
    host.disable_server_cache().disable_client_cache();

    host
}

pub fn agde(mut extensions: Extensions) -> Host {
    extensions.add_prepare_fn(
        // allow Let's Encrypt requests through
        Box::new(|req, _| !req.uri().path().starts_with("/.well-known")),
        prepare!(_, _, _, _, {
            FatResponse::no_cache(
                Response::builder()
                    .status(StatusCode::TEMPORARY_REDIRECT)
                    .header("location", "https://github.com/Icelk/agde/")
                    .body(Bytes::new())
                    .unwrap(),
            )
            .with_server_cache(comprash::ServerCachePreference::Full)
        }),
        Id::new(0, "redirect to GitHub"),
    );

    let mut host = host_from_name("agde.dev", "../agde.dev/", extensions);

    host.disable_client_cache().disable_server_cache();

    host
}

pub fn icelk_bitwarden_extensions() -> Extensions {
    let mut extensions = Extensions::empty();
    let ws_rev_proxy = kvarn_extensions::ReverseProxy::base(
        "/notifications/hub",
        kvarn_extensions::static_connection(kvarn_extensions::Connection::Tcp(
            kvarn_extensions::localhost(3012),
        )),
        Duration::from_secs(15),
    )
    .with_priority(-120);
    let rev_proxy = kvarn_extensions::ReverseProxy::base(
        "/",
        kvarn_extensions::static_connection(kvarn_extensions::Connection::Tcp(
            kvarn_extensions::localhost(8000),
        )),
        Duration::from_secs(15),
    )
    .with_x_real_ip();
    rev_proxy.mount(&mut extensions);
    ws_rev_proxy.mount(&mut extensions);
    kvarn_extensions::force_cache(
        &mut extensions,
        &[
            (".html", ClientCachePreference::Changing),
            (".woff2", ClientCachePreference::Full),
            (".woff", ClientCachePreference::Full),
            (".png", ClientCachePreference::Full),
            (".svg", ClientCachePreference::Changing),
            (".js", ClientCachePreference::Changing),
            (".css", ClientCachePreference::Changing),
        ],
    );

    extensions.add_prepare_fn(
        Box::new(|req, _| req.uri().path().starts_with("/.well-known")),
        prepare!(req, host, _, _, {
            let path = format!("/usr/share/webapps/vaultwarden-web{}", req.uri().path());
            let file = read::file(&path, host.file_cache.as_ref()).await;
            let file = if let Some(f) = file {
                f
            } else {
                return default_error_response(StatusCode::NOT_FOUND, host, None).await;
            };
            FatResponse::no_cache(Response::new(file))
        }),
        Id::new(1000, "override Let's Encrypt path"),
    );

    // Disable, to let reverse proxies' CSP through.
    extensions.with_csp(Csp::empty().arc());

    extensions
}
pub fn icelk_bitwarden(extensions: Extensions) -> Host {
    let mut host = host_from_name(
        "bitwarden.icelk.dev",
        "/usr/share/webapps/vaultwarden-web",
        extensions,
    );
    host.disable_server_cache().disable_client_cache();
    host
}

pub fn mail_hosts(file: impl AsRef<Path>) -> Vec<Host> {
    let file = match std::fs::read(file.as_ref()) {
        Ok(f) => f,
        Err(err) => {
            warn!(
                "Failed to read mail hosts file '{}': {}",
                file.as_ref().display(),
                err
            );
            return Vec::new();
        }
    };

    let file = String::from_utf8_lossy(&file);
    file.lines()
        .filter_map(|domain| {
            let domain = domain.trim();
            if domain.is_empty() {
                None
            } else {
                info!("Setting up host '{domain}'.");
                Some(Host::unsecure(
                    domain,
                    "mail",
                    Extensions::default(),
                    host::Options::default(),
                ))
            }
        })
        .collect()
}

fn host_from_name(name: &'static str, path: impl AsRef<Path>, extensions: Extensions) -> Host {
    #[cfg(feature = "https")]
    {
        let mut iter = name.split('.').rev();
        iter.next();
        let cert_base = utils::join(iter.rev(), ".");
        Host::http_redirect_or_unsecure(
            name,
            format!("{}-cert.pem", &cert_base),
            format!("{}-pk.pem", &cert_base),
            path.as_ref(),
            extensions,
            host::Options::default(),
        )
    }
    #[cfg(not(feature = "https"))]
    {
        Host::non_secure(name, path.as_ref(), extensions, host::Options::default())
    }
}