refactor: parse Basic auth via axum-extra typed header
commit
4b74b3crefactor: 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
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
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()