Skip to content

Commit e904731

Browse files
committed
refactor: replace hyper with reqwest for git proxy implementation
1 parent c7637ca commit e904731

File tree

7 files changed

+78
-41
lines changed

7 files changed

+78
-41
lines changed

Cargo.lock

Lines changed: 6 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ resolver = "2"
77
[package]
88
name = "backend"
99
description = "Backend API and services for StackClass"
10-
version = "0.33.0"
10+
version = "0.34.0"
1111
edition = "2024"
1212

1313
default-run = "stackclass-server"
@@ -33,7 +33,6 @@ gitea-client = { path = "crates/gitea-client" }
3333
anyhow = "1.0.98"
3434
axum = { version = "0.8.4" }
3535
axum-extra = {version = "0.10.1", features = ["typed-header"] }
36-
base64 = "0.22.1"
3736
chrono = { version = "0.4.41", features = ["serde"] }
3837
clap = { version = "4.5.41", features = ["derive", "env"] }
3938
dotenv = "0.15.0"
@@ -42,12 +41,12 @@ fs_extra = "1.3.0"
4241
futures = "0.3.31"
4342
ghrepo = "0.7.1"
4443
http-body-util = "0.1.3"
45-
hyper-util = {version = "0.1.16", features = ["client-legacy"] }
4644
indexmap = {version = "2.9.0", features = ["serde"] }
4745
jsonwebtoken = "9.3.1"
4846
k8s-openapi = { version = "0.25", default-features = false, features = ["latest"] }
4947
kube = { version = "1", default-features = false, features = ["runtime", "derive", "rustls-tls"] }
5048
octocrab = "0.44.1"
49+
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
5150
serde = { version = "1.0.219", features = ["derive"] }
5251
serde_json = "1.0.141"
5352
serde_yml = "0.0.12"

openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"license": {
77
"name": ""
88
},
9-
"version": "0.33.0"
9+
"version": "0.34.0"
1010
},
1111
"paths": {
1212
"/v1/courses": {

src/context.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use axum::body::Body;
1615
use gitea_client::GiteaClient;
17-
use hyper_util::{
18-
client::legacy::{Client, connect::HttpConnector},
19-
rt::TokioExecutor,
20-
};
16+
use reqwest::Client;
2117

2218
use crate::{config::Config, database::Database, errors::Result};
2319

@@ -36,7 +32,7 @@ pub struct Context {
3632
pub k8s: kube::Client,
3733

3834
/// HTTP client for making external requests
39-
pub http: Client<HttpConnector, Body>,
35+
pub http: Client,
4036
}
4137

4238
impl Context {
@@ -48,7 +44,7 @@ impl Context {
4844
config.git_server_password.clone(),
4945
);
5046
let k8s = kube::Client::try_default().await?;
51-
let http = Client::builder(TokioExecutor::new()).build_http();
47+
let http = Client::new();
5248

5349
Ok(Context { config, database, git, k8s, http })
5450
}

src/extractor.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use jsonwebtoken::{
3131
use serde::{Deserialize, Serialize};
3232
use thiserror::Error;
3333
use tokio::sync::{OnceCell, RwLock};
34-
use tracing::{debug, error};
34+
use tracing::{debug, error, info};
3535

3636
use crate::{context::Context, errors::AutoIntoResponse, repository::UserRepository};
3737

@@ -41,6 +41,7 @@ static KEYS: OnceCell<Arc<RwLock<HashMap<String, DecodingKey>>>> = OnceCell::con
4141
/// Loads JSON Web Keys (JWKs) from the database and converts them into `DecodingKey` instances.
4242
/// Returns a `HashMap` mapping key IDs to their corresponding `DecodingKey`.
4343
async fn load_keys(ctx: Arc<Context>) -> Result<HashMap<String, DecodingKey>, ClaimsError> {
44+
info!("Fetching all JSON Web Keys (JWKS) from the database");
4445
let keys = UserRepository::find_all_json_web_keys(&ctx.database).await.map_err(|e| {
4546
error!("Failed to load JSON web keys: {}", e);
4647
ClaimsError::KeyLoadFailure

src/handler/git.rs

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
use std::sync::Arc;
1616

1717
use axum::{
18-
body::Body,
18+
body::{self, Body},
1919
extract::{Path, State},
2020
http::{Request, StatusCode, Uri, header},
21-
response::IntoResponse,
21+
response::{IntoResponse, Response},
2222
};
23-
use base64::{Engine, engine::general_purpose::STANDARD as Base64};
24-
use tracing::{debug, error, warn};
23+
use tracing::{error, info, trace, warn};
2524
use uuid::Uuid;
2625

2726
use crate::{
@@ -36,11 +35,9 @@ use crate::{
3635
/// This function handles authentication and routing for Git operations.
3736
pub async fn proxy(
3837
State(ctx): State<Arc<Context>>,
39-
Path((uuid, path)): Path<(Uuid, String)>,
40-
mut req: Request<Body>,
38+
Path((uuid, _)): Path<(Uuid, String)>,
39+
req: Request<Body>,
4140
) -> impl IntoResponse {
42-
debug!(%uuid, %path, "Proxying Git request");
43-
4441
// Look up repository information by UUID (user course ID)
4542
// Return NOT_FOUND if repository is invalid or doesn't exist
4643
let (owner, repo, email) = lookup(&ctx.database, &uuid).await.ok_or_else(|| {
@@ -51,24 +48,62 @@ pub async fn proxy(
5148
let Config { git_server_endpoint, auth_secret, .. } = &ctx.config;
5249

5350
// Construct the URI for the Git server request to Gitea backend.
54-
let sanitized_path = path.trim_start_matches('/');
55-
let uri_str = format!("{git_server_endpoint}/{owner}/{repo}.git/{}", sanitized_path);
56-
*req.uri_mut() = Uri::try_from(uri_str).map_err(|e| {
57-
error!(error = %e, "Failed to construct URI for proxy destination");
51+
let trimmed = strip_uuid_prefix(req.uri(), &uuid);
52+
let url = format!("{git_server_endpoint}/{owner}/{repo}.git{trimmed}");
53+
let url = reqwest::Url::parse(&url).map_err(|e| {
54+
error!(error = %e, "Failed to parse URI for proxy destination");
55+
StatusCode::INTERNAL_SERVER_ERROR
56+
})?;
57+
info!(url = %url, "Forwarding to Git server");
58+
59+
// Convert axum Request to reqwest Request
60+
let (mut parts, body) = req.into_parts();
61+
let body_bytes = body::to_bytes(body, usize::MAX).await.map_err(|e| {
62+
error!(error = %e, "Failed to read request body");
5863
StatusCode::INTERNAL_SERVER_ERROR
5964
})?;
6065

61-
// Generate a password for the user's email using the auth secret and then
62-
// construct the Basic Auth header value for the Git server request.
66+
// Remove the original host header
67+
parts.headers.remove(header::HOST);
6368
let password = crypto::password(&email, auth_secret);
64-
let credentials = Base64.encode(format!("{owner}:{password}"));
65-
let auth_header_value = format!("Basic {credentials}").parse().unwrap();
66-
req.headers_mut().insert(header::AUTHORIZATION, auth_header_value);
6769

68-
debug!(uri = %req.uri(), "Forwarding to Git server");
69-
ctx.http.request(req).await.map_err(|e| {
70+
let request = ctx
71+
.http
72+
.request(parts.method, url)
73+
.headers(parts.headers)
74+
.basic_auth(owner, Some(password))
75+
.body(body_bytes.to_vec())
76+
.build()
77+
.map_err(|e| {
78+
error!(error = %e, "Failed to build reqwest request");
79+
StatusCode::INTERNAL_SERVER_ERROR
80+
})?;
81+
trace!(?request, "Built client request for Git server");
82+
83+
// Execute the request
84+
let response = ctx.http.execute(request).await.map_err(|e| {
7085
error!(error = %e, "Git server request failed");
7186
StatusCode::BAD_GATEWAY
87+
})?;
88+
trace!(?response, "Received response from Git server");
89+
90+
// Convert reqwest Response to axum Response
91+
let status = response.status();
92+
let headers = response.headers().clone();
93+
let body = response.bytes().await.map_err(|e| {
94+
error!(error = %e, "Failed to read response body");
95+
StatusCode::INTERNAL_SERVER_ERROR
96+
})?;
97+
trace!(body = ?String::from_utf8_lossy(&body), "Git server response");
98+
99+
let mut response_builder = Response::builder().status(status);
100+
for (key, value) in headers.iter() {
101+
response_builder = response_builder.header(key, value);
102+
}
103+
104+
response_builder.body(Body::from(body)).map_err(|e| {
105+
error!(error = %e, "Failed to build response");
106+
StatusCode::INTERNAL_SERVER_ERROR
72107
})
73108
}
74109

@@ -79,3 +114,13 @@ async fn lookup(db: &Database, uuid: &Uuid) -> Option<(String, String, String)>
79114

80115
Some((user.username(), course.course_slug, user.email))
81116
}
117+
118+
/// Strip the leading "/{uuid}" from a request URI and return the remaining path+query.
119+
/// For example:
120+
/// input: "/5a0e.../info/refs?service=git-receive-pack"
121+
/// output: "/info/refs?service=git-receive-pack"
122+
fn strip_uuid_prefix(uri: &Uri, uuid: &Uuid) -> String {
123+
let path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("");
124+
let prefix = format!("/{}", uuid);
125+
path_and_query.strip_prefix(&prefix).unwrap_or(path_and_query).to_string()
126+
}

src/repository/user.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use tracing::debug;
16-
1715
use crate::{
1816
database::Database,
1917
model::{JsonWebKey, UserModel},
@@ -26,7 +24,6 @@ pub struct UserRepository;
2624
impl UserRepository {
2725
/// Fetch all JSON Web Keys (JWKS) from the database.
2826
pub async fn find_all_json_web_keys(db: &Database) -> Result<Vec<JsonWebKey>> {
29-
debug!("Fetching all JSON Web Keys (JWKS) from the database");
3027
let keys =
3128
sqlx::query_as::<_, JsonWebKey>("SELECT * FROM jwks").fetch_all(db.pool()).await?;
3229
Ok(keys)

0 commit comments

Comments
 (0)