Parent [>]
1mod archive;
2mod branch;
3mod commit;
4mod raw;
5mod repo;
6mod tag;
7mod tree;
8
9use std::collections::HashMap;
10use std::path::{Component, PathBuf};
11
12use axum::Router;
13use axum::extract::{FromRequestParts, Path, State};
14use axum::http::header;
15use axum::http::request::Parts;
16use axum::response::IntoResponse;
17use axum::routing::get;
18
19use crate::error::{Error, Result};
20
21/// Axum extractor that pulls the `{repo}` path parameter and validates it
22/// contains no path traversal components before passing it to handlers.
23pub struct RepoName(pub String);
24
25impl<S: Send + Sync> FromRequestParts<S> for RepoName {
26    type Rejection = Error;
27
28    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
29        let Path(params) = Path::<HashMap<String, String>>::from_request_parts(parts, state)
30            .await
31            .map_err(|e| Error::BadRequest(e.to_string()))?;
32
33        let name = params
34            .get("repo")
35            .ok_or_else(|| Error::BadRequest("missing repo parameter".to_string()))?
36            .clone();
37
38        // Reject names containing non-normal components (e.g. `..`) to prevent
39        // directory traversal.
40        if std::path::Path::new(&name)
41            .components()
42            .any(|c| !matches!(c, Component::Normal(_)))
43        {
44            return Err(Error::BadRequest("invalid repository name".to_string()));
45        }
46
47        Ok(RepoName(name))
48    }
49}
50
51/// Shared application state passed to all route handlers.
52#[derive(Clone)]
53pub struct AppState {
54    /// Path to the directory containing bare git repositories.
55    pub root: PathBuf,
56    /// Concatenated CSS served at `/style.css`.
57    pub css: String,
58}
59
60/// Serves the concatenated stylesheet.
61async fn style(State(state): State<AppState>) -> impl IntoResponse {
62    (
63        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
64        state.css,
65    )
66}
67
68/// Builds the application router with all routes and shared state attached.
69pub fn router(state: AppState) -> Router {
70    Router::new()
71        .route("/style.css", get(style))
72        .route("/", get(repo::list))
73        .route("/{repo}", get(repo::detail))
74        .route("/{repo}/tags", get(tag::list))
75        .route("/{repo}/tags/{*tag}", get(tag::detail))
76        .route("/{repo}/branches", get(branch::list))
77        .route("/{repo}/branches/{*branch}", get(branch::detail))
78        .route("/{repo}/commits", get(commit::list))
79        .route("/{repo}/commits/{sha}", get(commit::detail))
80        .route("/{repo}/commits/{sha}/tree", get(tree::handler))
81        .route("/{repo}/commits/{sha}/tree/{*path}", get(tree::handler))
82        .route("/{repo}/commits/{sha}/raw/{*path}", get(raw::handler))
83        .route("/{repo}/archive/{format}/{*ref}", get(archive::handler))
84        .with_state(state)
85}