diff --git a/Cargo.lock b/Cargo.lock index b908cb9..9a01439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "async-trait" version = "0.1.77" @@ -57,6 +66,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_urlencoded", "sync_wrapper", "tokio", "tower", @@ -99,6 +109,27 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bytes" version = "1.5.0" @@ -117,18 +148,75 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -287,6 +375,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "2.2.4" @@ -321,6 +415,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.3" @@ -400,6 +503,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -411,11 +520,14 @@ name = "phone" version = "0.1.0" dependencies = [ "axum", + "color-eyre", + "fancy-regex", "serde", "thiserror", "tokio", "toml", "tower", + "tower-http", "tracing", "tracing-subscriber", ] @@ -470,6 +582,50 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -482,6 +638,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "serde" version = "1.0.197" @@ -511,6 +673,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -696,10 +870,26 @@ dependencies = [ ] [[package]] -name = "tower-layer" -version = "0.3.2" +name = "tower-http" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" @@ -739,6 +929,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -756,10 +956,14 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/Cargo.toml b/Cargo.toml index 73624c9..f4bbdef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,10 @@ lto = "fat" axum = { version = "0.7.2", default-features = false, features = [ "http1", "tokio", + "query", ] } +color-eyre = "0.6.3" +fancy-regex = "0.14.0" serde = { version = "1.0.195", features = ["derive"] } thiserror = "1.0.51" tokio = { version = "1.35.1", features = [ @@ -22,5 +25,6 @@ tokio = { version = "1.35.1", features = [ ] } toml = "0.8.8" tower = { version = "0.4.13", default-features = false, features = ["limit"] } +tower-http = { version = "0.6.1", features = ["trace"] } tracing = "0.1.40" -tracing-subscriber = "0.3.18" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/src/config.rs b/src/config.rs index 1ae03e3..199d6cb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,21 @@ -use std::{ - collections::HashMap, - error::Error, - net::{IpAddr, Ipv4Addr}, - path::{Path, PathBuf}, -}; +use std::collections::HashMap; +use std::env; +use std::net::{IpAddr, Ipv4Addr}; +use std::path::PathBuf; +use color_eyre::eyre::{self, bail, Context}; +use fancy_regex::Regex; use serde::{Deserialize, Serialize}; -use tokio::io::AsyncReadExt; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tracing::{error, info, instrument}; + +#[derive(Deserialize, Serialize, Debug)] +#[repr(transparent)] +pub struct DomainMatcher(#[serde(with = "regex")] Regex); + +pub fn cast_matcher(v: &[DomainMatcher]) -> &[Regex] { + unsafe { std::mem::transmute(v) } +} #[derive(Serialize, Deserialize, Debug)] #[serde(default)] @@ -15,6 +24,7 @@ pub struct Config { pub port: u16, pub concurrency: usize, pub repos: HashMap, + pub domains: Vec, } impl Default for Config { @@ -24,6 +34,7 @@ impl Default for Config { port: 5000, concurrency: 8, repos: HashMap::new(), + domains: Vec::new(), } } } @@ -46,15 +57,87 @@ pub struct SecretRule { pub value: String, } -pub async fn load(file: impl AsRef) -> Result> { - let mut buf = String::new(); - - tokio::fs::OpenOptions::new() +#[instrument(name = "config")] +pub async fn load() -> eyre::Result { + let config_file = env::var(format!( + "{}_CONFIG", + env!("CARGO_BIN_NAME").to_uppercase().replace('-', "_") + )) + .unwrap_or(String::from("config.toml")); + match tokio::fs::OpenOptions::new() .read(true) - .open(&file) - .await? - .read_to_string(&mut buf) - .await?; - - Ok(toml::from_str(&buf)?) + .open(&config_file) + .await + { + Ok(mut file) => { + let mut buf = String::new(); + file.read_to_string(&mut buf) + .await + .context("couldn't read configuration file")?; + toml::from_str(&buf).context("couldn't parse configuration") + } + Err(err) => match err.kind() { + std::io::ErrorKind::NotFound => { + let config = Config::default(); + info!("configuration file doesn't exist, creating"); + match tokio::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&config_file) + .await + { + Ok(mut file) => file + .write_all( + toml::to_string_pretty(&config) + .context("couldn't serialize configuration")? + .as_bytes(), + ) + .await + .unwrap_or_else(|err| error!("couldn't write configuration: {}", err)), + Err(err) => { + error!("couldn't open file {:?} for writing: {}", &config_file, err) + } + } + Ok(config) + } + _ => bail!("couldn't open config file: {}", err), + }, + } +} + +mod regex { + use fancy_regex::Regex; + use serde::{de::Visitor, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(d: D) -> Result + where + D: Deserializer<'de>, + { + struct RegexVisitor; + impl<'v> Visitor<'v> for RegexVisitor { + type Value = Regex; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a regex string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let regex = Regex::new(v); + regex.map_err(|err| serde::de::Error::custom(err)) + } + } + + d.deserialize_string(RegexVisitor) + } + + pub fn serialize(value: &Regex, serializer: S) -> Result + where + S: Serializer, + { + value.as_str().serialize(serializer) + } } diff --git a/src/main.rs b/src/main.rs index cf42ec2..60ff2e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,27 @@ -#![feature(let_chains, fs_try_exists, io_error_more)] +#![feature(let_chains, io_error_more)] mod config; mod update_repo; -use std::{env, sync::Arc}; +use std::sync::Arc; +use std::time::Duration; -use axum::{ - extract::{Path, State}, - http::{HeaderMap, StatusCode}, - response::{IntoResponse, Response}, - routing::post, - Router, -}; -use config::{Config, SecretRule}; +use axum::extract::{Path, Query, State}; +use axum::http::{HeaderMap, Request, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::Router; +use color_eyre::eyre::{self, Context}; +use config::{cast_matcher, Config, SecretRule}; use thiserror::Error; use tokio::net::TcpListener; use tower::limit::ConcurrencyLimitLayer; -use tracing::error; +use tower_http::trace::TraceLayer; +use tracing::level_filters::LevelFilter; +use tracing::{error, info, info_span, Span}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; use update_repo::update_repo; use crate::config::RepoConfig; @@ -63,14 +68,37 @@ async fn update_repo_handler( } } -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); +#[derive(serde::Deserialize)] +struct AskQuery { + domain: String, +} - let mut config: Config = - config::load(env::var("PHONE_CONFIG").unwrap_or(String::from("config.toml"))) - .await - .unwrap_or_else(|err| panic!("failed to load config: {}", err)); +async fn ask( + State(config): State>, + Query(AskQuery { domain }): Query, +) -> StatusCode { + for matcher in cast_matcher(&config.domains) { + if matcher.is_match(&domain).is_ok_and(|x| x) { + return StatusCode::OK; + } + } + + StatusCode::NOT_FOUND +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + color_eyre::install()?; + tracing_subscriber::registry() + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let mut config: Config = config::load().await.context("failed to load config")?; config.repos = config .repos @@ -98,12 +126,32 @@ async fn main() { let app = Router::new() .route("/update_repo/:repo", post(update_repo_handler)) .layer(ConcurrencyLimitLayer::new(8)) - .with_state(Arc::clone(&config)); + .route("/ask", get(ask)) + .with_state(Arc::clone(&config)) + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &Request<_>| { + info_span!( + "request", + method = ?request.method(), + path = ?request.uri().path(), + ) + }) + .on_response(|response: &Response<_>, duration: Duration, span: &Span| { + let _ = span.enter(); + let status = response.status(); + info!(?status, ?duration, "response"); + }), + ); let address = (config.host, config.port); - let listener = TcpListener::bind(address).await.unwrap(); - let local_addr = listener.local_addr().unwrap(); - tracing::info!("listening on http://{}", local_addr); + let listener = TcpListener::bind(address).await.context("failed to bind")?; + let local_addr = listener.local_addr().context("failed to get local addr")?; + info!("listening on http://{}", local_addr); - axum::serve(listener, app).await.unwrap(); + axum::serve(listener, app) + .await + .context("failed to serve app")?; + + Ok(()) }