git-ents.gitmain
⌘K
foforge
commit 4b74b3c
refactor: parse Basic auth via axum-extra typed header

Replace the hand-rolled base64 decoder and Authorization header parsing with TypedHeader<Authorization<Basic>>, comparing the password field against the expected token.

refactor: extract credentials with axum-extra typed header deprecates: hand-rolled base64_decode helper Assisted-by: Claude:claude-opus-4-8

Joseph D. Carpinelli · 2 months ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

Cargo.lock @@ -110,12 +110,49 @@ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-core", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.12.0" @@ -184,6 +221,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "errno" version = "0.3.14" @@ -242,6 +308,16 @@ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -262,12 +338,37 @@ version = "0.0.0" dependencies = [ "axum", + "axum-extra", "clap", "clap_mangen", "tempfile", "tokio", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.5.0" @@ -551,6 +652,17 @@ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -694,6 +806,12 @@ "once_cell", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -706,6 +824,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1"
Cargo.toml @@ -13,6 +13,7 @@ [workspace.dependencies] axum = "0.8" +axum-extra = { version = "0.12", features = ["typed-header"] } clap = { version = "4.5.60", features = ["derive"] } clap_mangen = "0.2.31" gix = { version = "0.83", features = ["no-default-features"] }
crates/git-ents-server/Cargo.toml @@ -7,6 +7,7 @@ [dependencies] axum = { workspace = true } +axum-extra = { workspace = true } clap = { workspace = true } clap_mangen = { workspace = true } tokio = { workspace = true }
crates/git-ents-server/src/http.rs @@ -12,6 +12,9 @@ use axum::extract::State; use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header}; use axum::response::{IntoResponse, Response}; +use axum_extra::TypedHeader; +use axum_extra::headers::Authorization; +use axum_extra::headers::authorization::Basic; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; @@ -27,13 +30,14 @@ /// Delegate a single request to `git http-backend` and reply with its output. pub async fn git( State(state): State<AppState>, + auth: Option<TypedHeader<Authorization<Basic>>>, method: Method, uri: Uri, headers: HeaderMap, body: Bytes, ) -> Response { if let Some(expected) = state.access_token.as_deref() - && !authorized(&headers, expected) + && !authorized(auth.as_ref(), expected) { return unauthorized(); } @@ -255,23 +259,8 @@ /// /// As with GitHub's HTTPS git auth, the username is ignored and the password /// field carries the bearer token. -fn authorized(headers: &HeaderMap, expected: &str) -> bool { - let Some(value) = header_value(headers, "Authorization") else { - return false; - }; - let Some(encoded) = value - .strip_prefix("Basic ") - .or_else(|| value.strip_prefix("basic ")) - else { - return false; - }; - let Some(decoded) = base64_decode(encoded.trim()) else { - return false; - }; - let Some(colon) = decoded.iter().position(|byte| *byte == b':') else { - return false; - }; - decoded.get(colon.saturating_add(1)..) == Some(expected.as_bytes()) +fn authorized(auth: Option<&TypedHeader<Authorization<Basic>>>, expected: &str) -> bool { + auth.is_some_and(|TypedHeader(creds)| creds.password() == expected) } /// A `401` carrying the Basic challenge git expects before retrying with creds. @@ -291,35 +280,6 @@ .map(str::to_owned) } -/// Decode standard base64 with optional `=` padding; `None` on any bad input. -fn base64_decode(input: &str) -> Option<Vec<u8>> { - fn sextet(byte: u8) -> Option<u32> { - let value = u32::from(byte); - match byte { - b'A'..=b'Z' => Some(value.saturating_sub(u32::from(b'A'))), - b'a'..=b'z' => Some(value.saturating_sub(u32::from(b'a')).saturating_add(26)), - b'0'..=b'9' => Some(value.saturating_sub(u32::from(b'0')).saturating_add(52)), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } - } - - let bytes: &[u8] = input.trim_end_matches('=').as_bytes(); - let mut out = Vec::new(); - let mut acc: u32 = 0; - let mut bits: u32 = 0; - for &byte in bytes { - acc = (acc << 6) | sextet(byte)?; - bits = bits.saturating_add(6); - if bits >= 8 { - bits = bits.saturating_sub(8); - out.push(u8::try_from((acc >> bits) & 0xFF).ok()?); - } - } - Some(out) -} - fn parse_status(value: &[u8]) -> Option<u16> { let token = value.split(|byte| *byte == b' ').next()?; std::str::from_utf8(token).ok()?.parse().ok()