diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7ca8185..436b455 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -57,18 +57,18 @@ jobs: - host: windows-latest build: yarn build --target i686-pc-windows-msvc target: i686-pc-windows-msvc - - host: ubuntu-latest + - host: ubuntu-22.04 target: x86_64-unknown-linux-gnu - build: yarn build --target x86_64-unknown-linux-gnu --use-napi-cross + build: yarn build --target x86_64-unknown-linux-gnu - host: ubuntu-latest target: x86_64-unknown-linux-musl build: yarn build --target x86_64-unknown-linux-musl -x - host: macos-latest target: aarch64-apple-darwin build: yarn build --target aarch64-apple-darwin - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-unknown-linux-gnu - build: yarn build --target aarch64-unknown-linux-gnu --use-napi-cross + build: yarn build --target aarch64-unknown-linux-gnu - host: ubuntu-latest target: armv7-unknown-linux-gnueabihf build: yarn build --target armv7-unknown-linux-gnueabihf --use-napi-cross @@ -124,6 +124,24 @@ jobs: run: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }} shell: bash + - name: Install build essentials (x86_64 Linux) + if: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' }} + run: | + sudo apt-get update + sudo apt install build-essential + echo "CC_x86_64_unknown_linux_gnu=gcc" >> $GITHUB_ENV + echo "CXX_x86_64_unknown_linux_gnu=g++" >> $GITHUB_ENV + echo "AR_x86_64_unknown_linux_gnu=ar" >> $GITHUB_ENV + + - name: Install build essentials (aarch64 Linux) + if: ${{ matrix.settings.target == 'aarch64-unknown-linux-gnu' }} + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu + echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV + echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - name: Install dependencies run: yarn install - name: Build diff --git a/Cargo.toml b/Cargo.toml index 2f6643b..01f6b78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,35 +31,37 @@ mime_guess = "2.0.5" napi-derive = "3.0.0" percent-encoding = "2.3.2" regex = "1.12.3" +rustls-acme = {version = "0.15.1", features = ["tokio"]} serde_json = "1.0.148" serde_qs = "1.0.0" serde_urlencoded = "0.7.1" tempfile = "3.24.0" +tokio-stream = {version = "0.1.18", features = ["net"]} urlencoding = "2.1.3" [dependencies.cookie] +features = ["percent-encode", "secure"] version = "0.18.1" -features = ["percent-encode","secure"] [dependencies.etag] -version = "4.0.0" features = ["std"] +version = "4.0.0" [dependencies.hyper] -version = "1.8.1" features = ["full"] +version = "1.8.1" [dependencies.hyper-util] -version = "0.1.19" features = ["tokio"] +version = "0.1.19" [dependencies.napi] +features = ["napi6", "async", "serde", "serde-json"] version = "3.0.0" -features = ["napi6","async","serde","serde-json"] [dependencies.tokio] +features = ["rt", "net", "rt-multi-thread", "macros"] version = "1.48.0" -features = ["rt","net","rt-multi-thread","macros"] [build-dependencies] napi-build = "2" diff --git a/index.d.ts b/index.d.ts index a615d4a..e8c83ad 100644 --- a/index.d.ts +++ b/index.d.ts @@ -770,6 +770,7 @@ export declare class Server { post(route: string, handler: JsHandlerFn): void put(route: string, handler: JsHandlerFn): void use(route: string | undefined | null, middleware: JsHandlerFn): void + acmeConfigMeta(config: AcmeConfigMeta): void listen(addr: string): void } @@ -945,6 +946,12 @@ export declare class Version { static http3(): Version } +export interface AcmeConfigMeta { + domains: Array + contactEmail: string + cacheDir: string +} + export interface ClearCookie { } diff --git a/server.ts b/server.ts index 814d72c..2f4cf0d 100644 --- a/server.ts +++ b/server.ts @@ -23,6 +23,16 @@ const __dirname = process.cwd() // Create app with router const app = new Server() +// ============================================================================ +// LETSENCRYPT: How to configure +// ============================================================================ + +// app.acmeConfigMeta({ +// domains: ['"example.com'], +// contactEmail: 'admin@foo.com', +// cacheDir: '/home/tomn/.local..', +// }) + // ============================================================================ // ROUTE DEFINITIONS // ============================================================================ diff --git a/src/server/mod.rs b/src/server/mod.rs index 70cacde..5d64e82 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -2,6 +2,7 @@ mod get_next_id; mod handle_http_request; use env_logger::Builder as EnvLoggerBuilder; +use futures::prelude::*; use hyper::Method as LibMethod; use hyper::{server::conn::http1, service::service_fn}; use hyper_util::rt::tokio::{TokioIo, TokioTimer}; @@ -10,8 +11,11 @@ use matchit::{InsertError, Router}; use napi::bindgen_prelude::*; use napi::threadsafe_function::{ThreadsafeCallContext, ThreadsafeFunction}; use napi_derive::napi; +use rustls_acme::AcmeConfig; +use rustls_acme::caches::DirCache; use std::sync::Arc; use tokio::net::TcpListener; +use tokio_stream::wrappers::TcpListenerStream; use crate::request::Request; use crate::response::Response; @@ -59,11 +63,20 @@ pub struct MiddlewareMeta { method: Option, } +#[napi(object)] +#[derive(Debug, Clone)] +pub struct AcmeConfigMeta { + pub domains: Vec, + pub contact_email: String, + pub cache_dir: String, +} + /// HTTP Server that integrates with JavaScript handlers via Router #[napi] pub struct Server { middlewares: Vec, router: Router, + acme_config_meta: Option, } impl Server { @@ -116,6 +129,7 @@ impl Server { Ok(Self { middlewares: Vec::new(), router: Router::new(), + acme_config_meta: None, }) } @@ -144,10 +158,16 @@ impl Server { self.register_middleware(route, middleware, env) } + #[napi] + pub fn acme_config_meta(&mut self, config: AcmeConfigMeta) { + self.acme_config_meta = Some(config) + } + #[napi] pub fn listen(&self, addr: String) -> Result<()> { let router = Arc::new(self.router.clone()); let middlewares = Arc::new(self.middlewares.clone()); + let acme_config_meta = self.acme_config_meta.clone(); EnvLoggerBuilder::new() .filter_level(LevelFilter::max()) @@ -156,27 +176,62 @@ impl Server { std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async move { - let listener = TcpListener::bind(&addr).await.unwrap(); + let tcp_listener = TcpListener::bind(&addr).await.unwrap(); log::debug!("Server listening on {}", addr); - loop { - let (socket, _) = listener.accept().await.unwrap(); - let io = TokioIo::new(socket); - - let router = router.clone(); - let middlewares = middlewares.clone(); - - tokio::task::spawn(async move { - let _ = http1::Builder::new() - .timer(TokioTimer::new()) - .serve_connection( - io, - service_fn(move |req| { - handle_http_request(req, router.clone(), middlewares.clone()) - }), - ) - .await; - }); + match acme_config_meta { + Some(acme) => { + let tcp_stream = TcpListenerStream::new(tcp_listener); + + let mut tls_incoming = AcmeConfig::new(acme.domains) + .contact_push(format!("mailto:{}", acme.contact_email)) + .cache(DirCache::new(acme.cache_dir)) + .tokio_incoming(tcp_stream, Vec::new()); + + while let Some(tls) = tls_incoming.next().await { + let tls = match tls { + Ok(t) => t, + Err(e) => { + log::error!("TLS accept error: {}", e); + continue; + } + }; + + let io = TokioIo::new(tls); + let router = router.clone(); + let middlewares = middlewares.clone(); + + tokio::task::spawn(async move { + let _ = http1::Builder::new() + .timer(TokioTimer::new()) + .serve_connection( + io, + service_fn(move |req| { + handle_http_request(req, router.clone(), middlewares.clone()) + }), + ) + .await; + }); + } + } + None => loop { + let (socket, _) = tcp_listener.accept().await.unwrap(); + let io = TokioIo::new(socket); + let router = router.clone(); + let middlewares = middlewares.clone(); + + tokio::task::spawn(async move { + let _ = http1::Builder::new() + .timer(TokioTimer::new()) + .serve_connection( + io, + service_fn(move |req| { + handle_http_request(req, router.clone(), middlewares.clone()) + }), + ) + .await; + }); + }, } }); });