diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ad9c4bc..14e604b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -27,6 +27,25 @@ jobs: - name: Run cargo test run: cargo test + lint: + name: Clippy + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: lint-cargo-${{ hashFiles('Cargo.lock') }} + restore-keys: lint-cargo- + + - name: cargo clippy + run: cargo clippy --all-targets + build-linux: name: Build – ${{ matrix.target }} runs-on: ubuntu-latest @@ -74,9 +93,9 @@ jobs: run: | set -e rustup update stable + rustup target add ${{ matrix.target }} npm ci --ignore-scripts if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then - rustup target add aarch64-unknown-linux-musl export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc export CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc fi diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d1cc2c1 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.94.1" +components = ["clippy", "rustfmt"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/src/http_listener.rs b/src/http_listener.rs index af4c701..4fe0770 100644 --- a/src/http_listener.rs +++ b/src/http_listener.rs @@ -1,17 +1,17 @@ -/// Plaintext HTTP/1.1 listener for ACME HTTP-01 challenges and HTTP→HTTPS redirects. -/// -/// Behaviour, per connection: -/// * Read the request headers. -/// * If the request target begins with `/.well-known/acme-challenge/`, look up -/// the route table by the `Host` header value (using the same wildcard rules -/// as SNI matching) and proxy the raw HTTP bytes to that route's first -/// upstream. This lets the `letsencrypt-cert-generator-fabric` Harper -/// component answer the challenge on its existing HTTP port. -/// * Anything else returns `301 Moved Permanently` with -/// `Location: https://`. -/// -/// This is intended to replace the standalone nginx :80 on Fabric hosts so -/// symphony alone can bind to both ports. +//! Plaintext HTTP/1.1 listener for ACME HTTP-01 challenges and HTTP→HTTPS redirects. +//! +//! Behaviour, per connection: +//! * Read the request headers. +//! * If the request target begins with `/.well-known/acme-challenge/`, look up +//! the route table by the `Host` header value (using the same wildcard rules +//! as SNI matching) and proxy the raw HTTP bytes to that route's first +//! upstream. This lets the `letsencrypt-cert-generator-fabric` Harper +//! component answer the challenge on its existing HTTP port. +//! * Anything else returns `301 Moved Permanently` with +//! `Location: https://`. +//! +//! This is intended to replace the standalone nginx :80 on Fabric hosts so +//! symphony alone can bind to both ports. use crate::http_proxy::{ host_header, read_http_headers, request_target, strip_body_framing, with_connection_close, @@ -55,7 +55,7 @@ pub async fn spawn_http_listeners( for _ in 0..workers { let socket = make_reuseport_socket(addr)?; - let listener = TcpListener::from_std(socket.into())?; + let listener = TcpListener::from_std(socket)?; let ctx2 = ctx.clone(); let max_conn = max_connections; let srx = shutdown_rx.resubscribe(); @@ -197,7 +197,7 @@ async fn proxy_acme( let upstream = upstream::connect(&route.destination, Some(peer_addr.ip()), UPSTREAM_CONNECT_TIMEOUT) .await - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| std::io::Error::other(e.to_string()))?; // ACME HTTP-01 challenges are GET requests with no body. Strip any // Content-Length / Transfer-Encoding headers so a client that lies about a diff --git a/src/http_proxy.rs b/src/http_proxy.rs index 780260a..2c5a8bd 100644 --- a/src/http_proxy.rs +++ b/src/http_proxy.rs @@ -1,12 +1,12 @@ -/// HTTP/1.1 header parsing and response rewriting utilities for the UDS proxy loop. -/// -/// Harper's UDS sockets send `Connection: close` per response even for HTTP/1.1 -/// clients. The TLS-terminating proxy loop in proxy_conn.rs uses these helpers -/// to: -/// 1. Frame individual request / response messages (find \r\n\r\n). -/// 2. Rewrite `Connection: close` → `Connection: keep-alive` in upstream -/// responses so downstream TLS connections are reused. -/// 3. Copy body bytes for Content-Length or read-until-close semantics. +//! HTTP/1.1 header parsing and response rewriting utilities for the UDS proxy loop. +//! +//! Harper's UDS sockets send `Connection: close` per response even for HTTP/1.1 +//! clients. The TLS-terminating proxy loop in proxy_conn.rs uses these helpers +//! to: +//! 1. Frame individual request / response messages (find \r\n\r\n). +//! 2. Rewrite `Connection: close` → `Connection: keep-alive` in upstream +//! responses so downstream TLS connections are reused. +//! 3. Copy body bytes for Content-Length or read-until-close semantics. use std::io; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; diff --git a/src/listener.rs b/src/listener.rs index 5863d4b..a90fe65 100644 --- a/src/listener.rs +++ b/src/listener.rs @@ -28,7 +28,7 @@ pub async fn spawn_listeners( for _ in 0..workers { let socket = make_reuseport_socket(addr)?; - let listener = TcpListener::from_std(socket.into())?; + let listener = TcpListener::from_std(socket)?; let ctx2 = ctx.clone(); let max_conn = max_connections; let srx = shutdown_rx.resubscribe(); diff --git a/src/protection.rs b/src/protection.rs index ed94ef5..8a4861d 100644 --- a/src/protection.rs +++ b/src/protection.rs @@ -167,10 +167,11 @@ impl ProtectionState { } // 3b. JA4 blocklist - if !cfg.ja4_blocklist.is_empty() && !peek_info.ja4.is_empty() { - if cfg.ja4_blocklist.contains(peek_info.ja4.as_str()) { - return Decision::Block(BlockReason::Ja4Blocked); - } + if !cfg.ja4_blocklist.is_empty() + && !peek_info.ja4.is_empty() + && cfg.ja4_blocklist.contains(peek_info.ja4.as_str()) + { + return Decision::Block(BlockReason::Ja4Blocked); } // 4. Require SNI diff --git a/src/proxy.rs b/src/proxy.rs index 64cf6c6..67b32b0 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -628,14 +628,15 @@ fn parse_resolve_spec(r: &JsResolveRoute) -> Result { }) } -fn parse_protection_config( - prot: &JsProtectionConfig, -) -> Result<( +/// Parsed protection config plus the derived allowlist, blocklist, and raw blocklist strings. +type ParsedProtectionConfig = ( crate::protection::ProtectionConfig, Vec, Vec, Vec, -)> { +); + +fn parse_protection_config(prot: &JsProtectionConfig) -> Result { let mut cfg = crate::protection::ProtectionConfig::default(); if let Some(rl) = &prot.rate_limit { @@ -673,13 +674,7 @@ fn parse_protection_config( }) .collect::>>()?; - let blocklist_strings: Vec = prot - .blocklist - .as_deref() - .unwrap_or(&[]) - .iter() - .cloned() - .collect(); + let blocklist_strings: Vec = prot.blocklist.as_deref().unwrap_or(&[]).to_vec(); let blocklist: Vec = blocklist_strings .iter() diff --git a/src/proxy_conn.rs b/src/proxy_conn.rs index 61f87dd..134a747 100644 --- a/src/proxy_conn.rs +++ b/src/proxy_conn.rs @@ -231,7 +231,7 @@ async fn proxy_via_tls( ) -> std::io::Result<()> { let mut upstream = upstream::connect(dest, Some(sf.peer_addr.ip()), ctx.upstream_connect_timeout) .await - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| std::io::Error::other(e.to_string()))?; // HTTP-header injection is only valid for a plaintext HTTP/1 upstream. An h2-negotiated // upstream receives binary frames, so text header insertion would corrupt them. @@ -253,7 +253,7 @@ async fn proxy_raw( ) -> std::io::Result<()> { let mut upstream = upstream::connect(dest, Some(sf.peer_addr.ip()), ctx.upstream_connect_timeout) .await - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| std::io::Error::other(e.to_string()))?; // Passthrough forwards raw TLS bytes — never a plaintext HTTP/1 stream, so header injection // is disabled (only PROXY protocol carriers apply here). diff --git a/src/sni.rs b/src/sni.rs index e161e17..5b0dc94 100644 --- a/src/sni.rs +++ b/src/sni.rs @@ -337,7 +337,7 @@ fn compute_ja4(legacy_version: u16, cipher_suites: &[u8], extensions: &[u8]) -> let list = &ext_data[2..2 + list_len.min(ext_data.len().saturating_sub(2))]; if !list.is_empty() { let proto_len = list[0] as usize; - if proto_len > 0 && 1 + proto_len <= list.len() { + if proto_len > 0 && proto_len < list.len() { alpn_first = Some(list[1..1 + proto_len].to_vec()); } }