Sam Jeston
History
165 lines·
5.8 KB
·Rust
| 1 | mod account; |
| 2 | pub mod assets; |
| 3 | mod code; |
| 4 | pub mod ctx; |
| 5 | pub mod error; |
| 6 | pub mod fmt; |
| 7 | mod git_http; |
| 8 | mod home; |
| 9 | mod pulls; |
| 10 | mod repo; |
| 11 | |
| 12 | use askama::Template; |
| 13 | use axum::Router; |
| 14 | use axum::extract::DefaultBodyLimit; |
| 15 | use axum::http::{HeaderValue, StatusCode, header}; |
| 16 | use axum::response::{Html, IntoResponse, Response}; |
| 17 | use axum::routing::{get, post}; |
| 18 | use axum_extra::extract::cookie::Cookie; |
| 19 | use tower_http::trace::TraceLayer; |
| 20 | |
| 21 | use crate::state::AppState; |
| 22 | pub use ctx::Ctx; |
| 23 | pub use error::AppError; |
| 24 | |
| 25 | pub type WebResult = Result<Response, AppError>; |
| 26 | |
| 27 | /// Fetch negotiation requests list every commit the client already has. |
| 28 | const GIT_BODY_LIMIT: usize = 64 * 1024 * 1024; |
| 29 | |
| 30 | pub fn router(state: AppState) -> Router { |
| 31 | Router::new() |
| 32 | .route("/", get(home::index)) |
| 33 | .route("/explore", get(home::explore)) |
| 34 | .route("/login", get(account::login_form).post(account::login)) |
| 35 | .route( |
| 36 | "/register", |
| 37 | get(account::register_form).post(account::register), |
| 38 | ) |
| 39 | .route("/logout", post(account::logout)) |
| 40 | .route("/new", get(repo::new_form).post(repo::create)) |
| 41 | .route("/settings", get(account::settings_redirect)) |
| 42 | .route( |
| 43 | "/settings/profile", |
| 44 | get(account::profile_form).post(account::update_profile), |
| 45 | ) |
| 46 | .route("/settings/password", post(account::change_password)) |
| 47 | .route("/settings/keys", get(account::keys).post(account::add_key)) |
| 48 | .route("/settings/keys/{id}/delete", post(account::delete_key)) |
| 49 | .route("/static/style.css", get(assets::stylesheet)) |
| 50 | .route("/static/app.js", get(assets::script)) |
| 51 | .route("/favicon.svg", get(assets::favicon)) |
| 52 | .route("/{owner}", get(home::profile)) |
| 53 | .route("/{owner}/{repo}", get(code::repo_home)) |
| 54 | .route("/{owner}/{repo}/tree/{*rest}", get(code::tree)) |
| 55 | .route("/{owner}/{repo}/blob/{*rest}", get(code::blob)) |
| 56 | .route("/{owner}/{repo}/raw/{*rest}", get(code::raw)) |
| 57 | .route("/{owner}/{repo}/archive/{*file}", get(code::archive)) |
| 58 | .route("/{owner}/{repo}/commits", get(code::commits_default)) |
| 59 | .route("/{owner}/{repo}/commits/{*rest}", get(code::commits)) |
| 60 | .route("/{owner}/{repo}/commit/{sha}", get(code::commit)) |
| 61 | .route("/{owner}/{repo}/branches", get(repo::branches)) |
| 62 | .route("/{owner}/{repo}/branches/delete", post(repo::delete_branch)) |
| 63 | .route("/{owner}/{repo}/tags", get(repo::tags)) |
| 64 | .route("/{owner}/{repo}/compare", get(pulls::compare_default)) |
| 65 | .route("/{owner}/{repo}/compare/{*spec}", get(pulls::compare)) |
| 66 | .route( |
| 67 | "/{owner}/{repo}/pulls", |
| 68 | get(pulls::list).post(pulls::create), |
| 69 | ) |
| 70 | .route("/{owner}/{repo}/pull/{number}", get(pulls::conversation)) |
| 71 | .route("/{owner}/{repo}/pull/{number}/commits", get(pulls::commits)) |
| 72 | .route("/{owner}/{repo}/pull/{number}/files", get(pulls::files)) |
| 73 | .route( |
| 74 | "/{owner}/{repo}/pull/{number}/comment", |
| 75 | post(pulls::comment), |
| 76 | ) |
| 77 | .route("/{owner}/{repo}/pull/{number}/merge", post(pulls::merge)) |
| 78 | .route( |
| 79 | "/{owner}/{repo}/pull/{number}/state", |
| 80 | post(pulls::set_state), |
| 81 | ) |
| 82 | .route("/{owner}/{repo}/pull/{number}/edit", post(pulls::edit)) |
| 83 | .route( |
| 84 | "/{owner}/{repo}/settings", |
| 85 | get(repo::settings).post(repo::update_settings), |
| 86 | ) |
| 87 | .route( |
| 88 | "/{owner}/{repo}/settings/collaborators", |
| 89 | post(repo::add_collaborator), |
| 90 | ) |
| 91 | .route( |
| 92 | "/{owner}/{repo}/settings/collaborators/remove", |
| 93 | post(repo::remove_collaborator), |
| 94 | ) |
| 95 | .route("/{owner}/{repo}/settings/delete", post(repo::delete)) |
| 96 | .route("/{owner}/{repo}/info/refs", get(git_http::info_refs)) |
| 97 | .route( |
| 98 | "/{owner}/{repo}/git-upload-pack", |
| 99 | post(git_http::upload_pack).layer(DefaultBodyLimit::max(GIT_BODY_LIMIT)), |
| 100 | ) |
| 101 | .route( |
| 102 | "/{owner}/{repo}/git-receive-pack", |
| 103 | post(git_http::receive_pack), |
| 104 | ) |
| 105 | .fallback(|| async { AppError::NotFound }) |
| 106 | .layer(axum::middleware::from_fn_with_state( |
| 107 | state.clone(), |
| 108 | ctx::middleware, |
| 109 | )) |
| 110 | .layer(TraceLayer::new_for_http()) |
| 111 | .with_state(state) |
| 112 | } |
| 113 | |
| 114 | pub async fn serve(state: AppState) -> anyhow::Result<()> { |
| 115 | let addr = state.config.http_addr; |
| 116 | let listener = tokio::net::TcpListener::bind(addr).await?; |
| 117 | tracing::info!("web interface listening on http://{addr}"); |
| 118 | axum::serve(listener, router(state)).await?; |
| 119 | Ok(()) |
| 120 | } |
| 121 | |
| 122 | pub fn render<T: Template>(template: &T) -> WebResult { |
| 123 | Ok(Html(template.render()?).into_response()) |
| 124 | } |
| 125 | |
| 126 | pub fn redirect(to: &str) -> Response { |
| 127 | let mut resp = StatusCode::SEE_OTHER.into_response(); |
| 128 | if let Ok(v) = HeaderValue::from_str(to) { |
| 129 | resp.headers_mut().insert(header::LOCATION, v); |
| 130 | } |
| 131 | resp |
| 132 | } |
| 133 | |
| 134 | pub fn with_cookie(mut resp: Response, cookie: Cookie<'_>) -> Response { |
| 135 | if let Ok(v) = HeaderValue::from_str(&cookie.to_string()) { |
| 136 | resp.headers_mut().append(header::SET_COOKIE, v); |
| 137 | } |
| 138 | resp |
| 139 | } |
| 140 | |
| 141 | /// Redirect that shows a one-off message on the next page. |
| 142 | pub fn redirect_flash(to: &str, kind: &str, message: &str) -> Response { |
| 143 | with_cookie(redirect(to), ctx::flash_cookie(kind, message)) |
| 144 | } |
| 145 | |
| 146 | /// Runs blocking repository work off the async runtime. |
| 147 | pub async fn blocking<T, F>(f: F) -> Result<T, AppError> |
| 148 | where |
| 149 | T: Send + 'static, |
| 150 | F: FnOnce() -> Result<T, AppError> + Send + 'static, |
| 151 | { |
| 152 | tokio::task::spawn_blocking(f) |
| 153 | .await |
| 154 | .map_err(|e| AppError::Internal(e.into()))? |
| 155 | } |
| 156 | |
| 157 | /// Returns `next` if it is a safe same-site path, otherwise `/`. |
| 158 | pub fn safe_next(next: Option<&str>) -> String { |
| 159 | match next { |
| 160 | Some(n) if n.starts_with('/') && !n.starts_with("//") && !n.starts_with("/\\") => { |
| 161 | n.to_string() |
| 162 | } |
| 163 | _ => "/".to_string(), |
| 164 | } |
| 165 | } |