diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..88d611d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +# Tell cargo to use the correct linker when cross-compiling +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9bbe900..4a4a632 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -12,14 +12,18 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-unknown-linux-gnu - uses: Swatinem/rust-cache@v2 + - name: install gcc-aarch64-linux-gnu + run: sudo apt install -y gcc-aarch64-linux-gnu - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} - name: ssh-keyscan run: | mkdir -p ~/.ssh - ssh-keyscan wlsd.foltz.io > ~/.ssh/known_hosts + ssh-keyscan wlsd.lightandsound.design > ~/.ssh/known_hosts chmod 600 ~/.ssh/known_hosts - name: deploy - run: scripts/deploy.sh wlsd.foltz.io + run: scripts/deploy.sh ec2-user@wlsd.lightandsound.design diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 902402a..e510b0d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,10 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: cargo test - uses: actions-rs/cargo@v1 - with: - command: test + - run: cargo test rustfmt: name: Format @@ -27,11 +24,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - name: cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: -- --check + - run: cargo fmt --check clippy: name: Lint @@ -42,7 +35,19 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: cargo clippy - uses: actions-rs/cargo@v1 + - run: cargo clippy -- -D warnings + + cross: + name: Cross-compile + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable with: - command: clippy + targets: aarch64-unknown-linux-gnu + - uses: Swatinem/rust-cache@v2 + - name: install gcc-aarch64-linux-gnu + run: sudo apt install -y gcc-aarch64-linux-gnu + - run: cargo build --target aarch64-unknown-linux-gnu diff --git a/Cargo.toml b/Cargo.toml index 2c085da..2edaac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,20 +14,25 @@ sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] } lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "pool", "smtp-transport", "tokio1", "tokio1-rustls-tls", "serde"] } tokio = { version = "1", features = ["rt-multi-thread", "fs", "net", "sync", "macros"] } rustls = "0.23" +rustls-acme = { version = "0.12", features = ["axum"] } anyhow = "1" tracing = "0.1" tracing-subscriber = "0.3" +futures = "0.3" serde = { version = "1", features = ["derive"] } toml = "0.8" rand = "0.8" -mimalloc = "*" + +# Add a little optimization to debug builds [profile.dev] opt-level = 1 +# And since they don't get recompiled often, fully optimize dependencies [profile.dev.package."*"] opt-level = 3 +# Production build with more intense optimization [profile.prod] inherits = "release" lto = true diff --git a/config/dev.toml b/config/dev.toml index 6161781..abea952 100644 --- a/config/dev.toml +++ b/config/dev.toml @@ -2,13 +2,10 @@ url = "https://localhost:4433" db = "db.sqlite" -[http] -addr = "0.0.0.0:8080" -[https] -addr = "0.0.0.0:4433" -cert = "config/selfsigned.cert" -key = "config/selfsigned.key" +[net] +http_addr = "[::]:8080" +https_addr = "[::]:4433" -[mail] +[email] addr = "smtp://localhost:1025" from = "WLSD " diff --git a/config/prod.toml b/config/prod.toml index 3b216f8..2e96645 100644 --- a/config/prod.toml +++ b/config/prod.toml @@ -1,14 +1,17 @@ [app] -url = "https://wlsd.foltz.io" +url = "https://wlsd.lightandsound.design" db = "db.sqlite" -[http] -addr = "0.0.0.0:80" -[https] -addr = "0.0.0.0:443" -cert = "config/selfsigned.cert" -key = "config/selfsigned.key" +[net] +http_addr = "[::]:80" +https_addr = "[::]:443" -[mail] +[acme] +domain = "wlsd.lightandsound.design" +email = "studio249@foltz.io" +dir = "acme" +prod = true + +[email] addr = "smtp://localhost:1080" from = "WLSD " diff --git a/rustfmt.toml b/rustfmt.toml index 9cd2724..a71193a 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,5 @@ max_width = 110 -chain_width = 100 -fn_call_width = 100 +chain_width = 80 +fn_call_width = 80 struct_lit_width = 75 single_line_if_else_max_width = 80 diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 5d85866..2fa91ab 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,29 +1,28 @@ #!/bin/bash set -euxo pipefail if [ $# -ne 1 ]; then - echo "Usage: scripts/deploy.sh " + echo "Usage: scripts/deploy.sh @" exit 1 fi -ssh root@$1 <<'EOS' +ssh $1 <<'EOS' # update -apt update -apt upgrade -y -apt install -y rsync +sudo yum update -y # create a user if ! id wlsd &>/dev/null; then - adduser --disabled-password --gecos "" wlsd + sudo adduser wlsd fi # add ssh keys cat > .ssh/authorized_keys < /etc/systemd/system/wlsd.service </dev/null [Unit] Description=WLSD After=network.target @@ -38,7 +37,7 @@ Restart=always [Install] WantedBy=multi-user.target EOF -systemctl daemon-reload -systemctl enable wlsd -#systemctl restart wlsd +sudo systemctl daemon-reload +sudo systemctl enable wlsd +sudo systemctl restart wlsd EOS diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 35783ed..5d40d14 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,16 +1,16 @@ #!/bin/bash set -euxo pipefail if [ $# -ne 1 ]; then - echo "Usage: scripts/deploy.sh " + echo "Usage: scripts/deploy.sh @" exit 1 fi -cargo build --profile prod +cargo build --profile prod --target aarch64-unknown-linux-gnu -rsync -Pavzr --delete assets templates config target/prod/wlsd root@$1:/home/wlsd/ -ssh root@$1 <<'EOS' -apt-get update -apt-get upgrade -y -setcap 'cap_net_bind_service=+ep' /home/wlsd/wlsd -systemctl restart wlsd +ls -l target/aarch64-unknown-linux-gnu/ +ls -l target/aarch64-unknown-linux-gnu/* +rsync --rsync-path="sudo rsync" -Pavzr --delete assets templates config target/aarch64-unknown-linux-gnu/prod/wlsd $1:/home/wlsd/ +ssh $1 <<'EOS' +sudo setcap 'cap_net_bind_service=+ep' /home/wlsd/wlsd +sudo systemctl restart wlsd EOS diff --git a/src/app.rs b/src/app/mod.rs similarity index 97% rename from src/app.rs rename to src/app/mod.rs index 2cb3ce8..69f41d7 100644 --- a/src/app.rs +++ b/src/app/mod.rs @@ -1,11 +1,13 @@ use std::{sync::Arc, time::Duration}; -use crate::*; +use crate::utils::{config::*, db::Db, email::Email}; +use tera::Tera; + use anyhow::Result; use axum::{ extract::{MatchedPath, Path, Query, Request, State}, http::{header, StatusCode}, - response::{Html, IntoResponse, Response}, + response::{Html, IntoResponse, Redirect, Response}, routing::{get, post}, Form, Router, }; @@ -20,15 +22,15 @@ struct AppState { config: Config, templates: Tera, db: Db, - mail: Mail, + mail: Email, } pub async fn build(config: Config) -> Result { - let state = app::AppState { + let state = AppState { config: config.clone(), templates: Tera::new("templates/*")?, db: Db::connect(&config.app.db).await?, - mail: Mail::connect(config.mail).await?, + mail: Email::connect(config.email).await?, }; let router = Router::new() diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index f98ccca..0000000 --- a/src/config.rs +++ /dev/null @@ -1,40 +0,0 @@ -use anyhow::Result; -use lettre::message::Mailbox; -use std::{net::SocketAddr, path::PathBuf}; - -pub async fn load(file: &str) -> Result { - let contents = tokio::fs::read_to_string(file).await?; - Ok(toml::from_str(&contents)?) -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct Config { - pub app: ServerConfig, - pub http: HttpConfig, - pub https: HttpsConfig, - pub mail: MailConfig, -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct ServerConfig { - pub url: String, - pub db: PathBuf, -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct HttpConfig { - pub addr: SocketAddr, -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct HttpsConfig { - pub addr: SocketAddr, - pub cert: PathBuf, - pub key: PathBuf, -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct MailConfig { - pub addr: String, - pub from: Mailbox, -} diff --git a/src/main.rs b/src/main.rs index a22ae67..a979197 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,63 @@ use anyhow::{Context, Result}; -use axum::{handler::HandlerWithoutStateExt as _, response::Redirect}; -use axum_server::tls_rustls::RustlsConfig; -use mimalloc::MiMalloc; -use tera::Tera; mod app; -mod config; -mod db; -mod mail; - -use config::*; -use db::Db; -use mail::Mail; +mod utils; -#[global_allocator] -static ALLOC: MiMalloc = MiMalloc; +use axum::{handler::HandlerWithoutStateExt, response::Redirect}; +use axum_server::tls_rustls::RustlsConfig; +use futures::StreamExt; +use utils::config::*; #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt().init(); - let file = std::env::args().nth(1).context("usage: wlsd ")?; - let config = config::load(&file).await.with_context(|| format!("loading config={file}"))?; + + // Load the server config + let file = std::env::args().nth(1).context("usage: wlsd ")?; + let config = Config::load(&file).await?; let app = app::build(config.clone()).await?.into_make_service(); tracing::info!("Live at {}", &config.app.url); - // Redirect HTTP to HTTPS + // Spawn an auxillary HTTP server which just redirects to HTTPS tokio::spawn(async move { let redirect = move || async move { Redirect::permanent(&config.app.url) }; - axum_server::bind(config.http.addr).serve(redirect.into_make_service()).await + axum_server::bind(config.net.http_addr) + .serve(redirect.into_make_service()) + .await }); - // Bind HTTPS - let rustls = RustlsConfig::from_pem_file(config.https.cert, config.https.key).await?; - axum_server::bind_rustls(config.https.addr, rustls).serve(app).await?; + // Spawn the main HTTPS server + match config.acme { + // If ACME is configured, request a TLS certificate from Let's Encrypt + Some(acme) => { + let mut acme = rustls_acme::AcmeConfig::new([&acme.domain]) + .contact_push(format!("mailto:{}", &acme.email)) + .cache(rustls_acme::caches::DirCache::new(acme.dir.clone())) + .directory_lets_encrypt(acme.prod) + .state(); + + let acceptor = acme.axum_acceptor(acme.default_rustls_config()); + + tokio::spawn(async move { + loop { + match acme.next().await.unwrap() { + Ok(ok) => tracing::info!("acme: {:?}", ok), + Err(err) => tracing::error!("acme: {}", err), + } + } + }); + + axum_server::bind(config.net.https_addr).acceptor(acceptor).serve(app).await?; + } + // Otherwise, use the bundled self-signed TLS cert + None => { + let cert = include_bytes!("../config/selfsigned.cert"); + let key = include_bytes!("../config/selfsigned.key"); + let rustls = RustlsConfig::from_pem(cert.into(), key.into()).await?; + axum_server::bind_rustls(config.net.https_addr, rustls).serve(app).await?; + } + } + Ok(()) } diff --git a/src/utils/config.rs b/src/utils/config.rs new file mode 100644 index 0000000..acbdceb --- /dev/null +++ b/src/utils/config.rs @@ -0,0 +1,50 @@ +use anyhow::{Context, Result}; +use lettre::message::Mailbox; +use std::{net::SocketAddr, path::PathBuf}; + +impl Config { + pub async fn load(file: &str) -> Result { + async fn load_inner(file: &str) -> Result { + let contents = tokio::fs::read_to_string(file).await?; + Ok(toml::from_str(&contents)?) + } + load_inner(file).await.with_context(|| format!("loading config={file}")) + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct Config { + pub app: AppConfig, + pub net: NetConfig, + pub acme: Option, + pub email: EmailConfig, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct AppConfig { + pub url: String, + pub db: PathBuf, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct NetConfig { + pub http_addr: SocketAddr, + pub https_addr: SocketAddr, +} + +/// LetsEncrypt ACME TLS certificate configuration. +#[derive(Clone, Debug, serde::Deserialize)] +pub struct AcmeConfig { + pub domain: String, + pub email: String, + /// Directory where certificates and credentials are stored. + pub dir: String, + /// Whether to use the production or staging ACME server. + pub prod: bool, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct EmailConfig { + pub addr: String, + pub from: Mailbox, +} diff --git a/src/db.rs b/src/utils/db.rs similarity index 100% rename from src/db.rs rename to src/utils/db.rs diff --git a/src/mail.rs b/src/utils/email.rs similarity index 60% rename from src/mail.rs rename to src/utils/email.rs index 4f37e81..9ffe592 100644 --- a/src/mail.rs +++ b/src/utils/email.rs @@ -4,16 +4,16 @@ use lettre::{ Message, SmtpTransport, Transport, }; -use crate::MailConfig; +use crate::EmailConfig; #[derive(Clone)] -pub struct Mail { +pub struct Email { addr: String, from: Mailbox, } -impl Mail { - pub async fn connect(config: MailConfig) -> Result { +impl Email { + pub async fn connect(config: EmailConfig) -> Result { // we need this for smtps let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); Ok(Self { addr: config.addr, from: config.from }) @@ -29,15 +29,3 @@ impl Mail { Ok(()) } } - -// state -// .mail -// .send( -// Message::builder() -// .from("Radio WLSD ".parse()?) -// .to("Jack Foltz ".parse()?) -// .subject("Hello, world!") -// .header(ContentType::TEXT_PLAIN) -// .body("Be happy!".to_string())?, -// ) -// .await; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..1badade --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod db; +pub mod email;