use std::borrow::Cow;
use std::collections::HashMap;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use axum::body::{Body, Bytes};
use axum::extract::ws::{Message, WebSocket};
use axum::extract::{FromRequest, Path, Query, State, WebSocketUpgrade};
use axum::http::header::{HeaderName, SET_COOKIE};
use axum::http::{header, HeaderValue, Request, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::{BoxError, Json};
use cookie::Key;
use futures::future::select_all;
use futures::{SinkExt, Stream, StreamExt};
use log::error;
use serde::Deserialize;
use tokio::fs::File;
use tokio::sync::mpsc::channel;
use tokio::sync::Mutex;
use tokio_stream::wrappers::ReceiverStream;
use tokio_util::io::ReaderStream;
use crate::application::Application;
use crate::model::auth::{Authentication, RegistryUserToken, RegistryUserTokenWithSecret};
use crate::model::cargo::{
CrateUploadResult, OwnersChangeQuery, OwnersQueryResult, RegistryUser, SearchResults, YesNoMsgResult, YesNoResult,
};
use crate::model::deps::DepsAnalysis;
use crate::model::docs::{DocGenJob, DocGenJobSpec};
use crate::model::packages::{CrateInfo, CrateInfoTarget};
use crate::model::stats::{DownloadStats, GlobalStats};
use crate::model::worker::{JobSpecification, JobUpdate, WorkerDescriptor, WorkerPublicData, WorkerRegistrationData};
use crate::model::{AppVersion, CrateVersion, RegistryInformation};
use crate::services::index::Index;
use crate::utils::apierror::{
error_backend_failure, error_invalid_request, error_not_found, error_unauthorized, specialize, ApiError,
};
use crate::utils::axum::auth::{AuthData, AxumStateForCookies};
use crate::utils::axum::embedded::{EmbeddedResources, WebappResource};
use crate::utils::axum::extractors::Base64;
use crate::utils::axum::sse::{Event, ServerSentEventStream};
use crate::utils::axum::{response, response_error, ApiResult};
use crate::utils::token::generate_token;
pub struct AxumState {
pub application: Arc<Application>,
pub cookie_key: Key,
pub webapp_resources: EmbeddedResources,
}
impl AxumStateForCookies for AxumState {
fn get_domain(&self) -> Cow<'static, str> {
Cow::Owned(self.application.configuration.web_domain.clone())
}
fn get_id_cookie_name(&self) -> Cow<'static, str> {
Cow::Borrowed("cratery-user")
}
fn get_cookie_key(&self) -> &Key {
&self.cookie_key
}
}
impl AxumState {
async fn get_webapp_resource(&self, path: &str) -> Option<WebappResource> {
if let Some(hot_reload_path) = self.application.configuration.web_hot_reload_path.as_ref() {
let mut final_path = PathBuf::from(hot_reload_path);
for element in path.split('/') {
final_path.push(element);
}
let file_name = final_path.file_name().and_then(|n| n.to_str()).unwrap();
let content_type = get_content_type(file_name);
let data = tokio::fs::read(&final_path).await.ok()?;
Some(WebappResource::HotReload {
content_type: content_type.to_string(),
data,
})
} else {
let resource = self.webapp_resources.get(path).cloned()?;
Some(WebappResource::Embedded(resource))
}
}
}
#[derive(Deserialize)]
pub struct PathInfoCrate {
package: String,
}
#[derive(Deserialize)]
pub struct PathInfoCrateVersion {
package: String,
version: String,
}
pub async fn get_root(State(state): State<Arc<AxumState>>) -> (StatusCode, [(HeaderName, HeaderValue); 2]) {
let target = format!("{}/webapp/index.html", state.application.configuration.web_public_uri);
(
StatusCode::FOUND,
[
(header::LOCATION, HeaderValue::from_str(&target).unwrap()),
(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")),
],
)
}
pub async fn get_favicon(State(state): State<Arc<AxumState>>) -> (StatusCode, [(HeaderName, HeaderValue); 2], &'static [u8]) {
let favicon = state.webapp_resources.get("favicon.png").unwrap();
(
StatusCode::OK,
[
(header::CONTENT_TYPE, HeaderValue::from_static(favicon.content_type)),
(header::CACHE_CONTROL, HeaderValue::from_static("max-age=3600")),
],
favicon.content,
)
}
fn get_auth_redirect(state: &AxumState) -> (StatusCode, [(HeaderName, HeaderValue); 2]) {
let nonce = generate_token(64);
let oauth_state = generate_token(32);
let target = format!(
"{}?response_type={}&redirect_uri={}&client_id={}&scope={}&nonce={}&state={}",
state.application.configuration.oauth_login_uri,
"code",
urlencoding::encode(&format!(
"{}/webapp/oauthcallback.html",
state.application.configuration.web_public_uri
)),
urlencoding::encode(&state.application.configuration.oauth_client_id),
urlencoding::encode(&state.application.configuration.oauth_client_scope),
nonce,
oauth_state
);
(
StatusCode::FOUND,
[
(header::LOCATION, HeaderValue::from_str(&target).unwrap()),
(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")),
],
)
}
pub async fn get_redirection_crate(
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
) -> (StatusCode, [(HeaderName, HeaderValue); 2]) {
let target = format!("/webapp/crate.html?crate={package}");
(
StatusCode::FOUND,
[
(header::LOCATION, HeaderValue::from_str(&target).unwrap()),
(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=3600, immutable"),
),
],
)
}
pub async fn get_redirection_crate_version(
Path(PathInfoCrateVersion { package, version }): Path<PathInfoCrateVersion>,
) -> (StatusCode, [(HeaderName, HeaderValue); 2]) {
let target = format!("/webapp/crate.html?crate={package}&version={version}");
(
StatusCode::FOUND,
[
(header::LOCATION, HeaderValue::from_str(&target).unwrap()),
(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=3600, immutable"),
),
],
)
}
pub async fn get_webapp_resource(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
request: Request<Body>,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 2], Cow<'static, [u8]>), StatusCode> {
let path = request.uri().path();
let path = &path["/webapp/".len()..];
if let Some(crate_name) = path.strip_prefix("crates/") {
let target = format!("/webapp/crate.html?crate={crate_name}");
return Ok((
StatusCode::FOUND,
[
(header::LOCATION, HeaderValue::from_str(&target).unwrap()),
(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=3600, immutable"),
),
],
Cow::Borrowed(&[]),
));
}
if path == "index.html" {
let is_authenticated = state.application.authenticate(&auth_data).await.is_ok();
if !is_authenticated {
let (code, headers) = get_auth_redirect(&state);
return Ok((code, headers, Cow::Borrowed(&[])));
}
}
let resource = state.get_webapp_resource(path).await;
match resource {
Some(resource) => Ok((
StatusCode::OK,
[
(header::CONTENT_TYPE, HeaderValue::from_str(resource.content_type()).unwrap()),
(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=3600, immutable"),
),
],
resource.into_data(),
)),
None => Err(StatusCode::NOT_FOUND),
}
}
pub async fn webapp_me(State(state): State<Arc<AxumState>>) -> (StatusCode, [(HeaderName, HeaderValue); 2]) {
let target = format!("{}/webapp/index.html", state.application.configuration.web_public_uri);
(
StatusCode::FOUND,
[
(header::LOCATION, HeaderValue::from_str(&target).unwrap()),
(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")),
],
)
}
pub async fn get_docs_resource(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
request: Request<Body>,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 2], Body), (StatusCode, [(HeaderName, HeaderValue); 1], Body)> {
let is_authenticated = state.application.authenticate(&auth_data).await.is_ok();
if !is_authenticated {
let (code, headers) = get_auth_redirect(&state);
return Ok((code, headers, Body::empty()));
}
let elements = request.uri().path().split('/').filter(|e| !e.is_empty()).collect::<Vec<_>>();
if elements.len() < 4 || elements[0] != "docs" || semver::Version::from_str(elements[2]).is_err() {
return Err((
StatusCode::NOT_FOUND,
[(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=3600, immutable"),
)],
Body::empty(),
));
}
let (target, rest_index) = if elements.len() >= 5
&& state
.application
.configuration
.self_known_targets
.iter()
.any(|t| elements[3] == t)
{
(elements[3], 4)
} else {
(state.application.configuration.self_toolchain_host.as_str(), 3)
};
let key = format!(
"{}/{}/{}/{}",
elements[1],
elements[2],
target,
elements[rest_index..].join("/")
);
let extension = get_content_type(&key);
match state.application.get_service_storage().download_doc_file(&key).await {
Ok(content) => Ok((
StatusCode::OK,
[
(header::CONTENT_TYPE, HeaderValue::from_static(extension)),
(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=3600, immutable"),
),
],
Body::from(content),
)),
Err(e) => {
let message = e.to_string();
Err((
StatusCode::NOT_FOUND,
[(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"))],
Body::from(message),
))
}
}
}
fn get_content_type(name: &str) -> &'static str {
let extension = name.rfind('.').map(|index| &name[(index + 1)..]);
match extension {
Some("html") => "text/html",
Some("css") => "text/css",
Some("js") => "text/javascript",
Some("gif") => "image/gif",
Some("png") => "image/png",
Some("jpeg") => "image/jpeg",
Some("bmp") => "image/bmp",
Some("webp") => "image/webp",
Some("svg") => "image/svg+xml",
Some("ico") => "image/x-icon",
_ => "application/octet-stream",
}
}
pub async fn api_v1_get_registry_information(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
) -> ApiResult<RegistryInformation> {
response(state.application.get_registry_information(&auth_data).await)
}
pub async fn api_v1_get_current_user(auth_data: AuthData, State(state): State<Arc<AxumState>>) -> ApiResult<RegistryUser> {
response(state.application.get_current_user(&auth_data).await)
}
pub async fn api_v1_login_with_oauth_code(
mut auth_data: AuthData,
State(state): State<Arc<AxumState>>,
body: Bytes,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 1], Json<RegistryUser>), (StatusCode, Json<ApiError>)> {
let code = String::from_utf8_lossy(&body);
let registry_user = state.application.login_with_oauth_code(&code).await.map_err(response_error)?;
let cookie = auth_data.create_id_cookie(&Authentication::new_user(registry_user.id, registry_user.email.clone()));
Ok((
StatusCode::OK,
[(SET_COOKIE, HeaderValue::from_str(&cookie.to_string()).unwrap())],
Json(registry_user),
))
}
pub async fn api_v1_logout(mut auth_data: AuthData) -> (StatusCode, [(HeaderName, HeaderValue); 1]) {
let cookie = auth_data.create_expired_id_cookie();
(
StatusCode::OK,
[(SET_COOKIE, HeaderValue::from_str(&cookie.to_string()).unwrap())],
)
}
pub async fn api_v1_get_user_tokens(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
) -> ApiResult<Vec<RegistryUserToken>> {
response(state.application.get_tokens(&auth_data).await)
}
#[derive(Deserialize)]
pub struct CreateTokenQuery {
#[serde(rename = "canWrite")]
can_write: bool,
#[serde(rename = "canAdmin")]
can_admin: bool,
}
pub async fn api_v1_create_user_token(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Query(CreateTokenQuery { can_write, can_admin }): Query<CreateTokenQuery>,
name: String,
) -> ApiResult<RegistryUserTokenWithSecret> {
response(state.application.create_token(&auth_data, &name, can_write, can_admin).await)
}
pub async fn api_v1_revoke_user_token(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(token_id): Path<i64>,
) -> ApiResult<()> {
response(state.application.revoke_token(&auth_data, token_id).await)
}
pub async fn api_v1_get_global_tokens(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
) -> ApiResult<Vec<RegistryUserToken>> {
response(state.application.get_global_tokens(&auth_data).await)
}
pub async fn api_v1_create_global_token(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
name: String,
) -> ApiResult<RegistryUserTokenWithSecret> {
response(state.application.create_global_token(&auth_data, &name).await)
}
pub async fn api_v1_revoke_global_token(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(token_id): Path<i64>,
) -> ApiResult<()> {
response(state.application.revoke_global_token(&auth_data, token_id).await)
}
pub async fn api_v1_get_doc_gen_jobs(auth_data: AuthData, State(state): State<Arc<AxumState>>) -> ApiResult<Vec<DocGenJob>> {
response(state.application.get_doc_gen_jobs(&auth_data).await)
}
pub async fn api_v1_get_doc_gen_job_log(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(job_id): Path<i64>,
) -> ApiResult<String> {
response(state.application.get_doc_gen_job_log(&auth_data, job_id).await)
}
pub async fn api_v1_get_doc_gen_job_updates(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
) -> Result<Response, (StatusCode, Json<ApiError>)> {
let receiver = match state.application.get_doc_gen_job_updates(&auth_data).await {
Ok(r) => r,
Err(e) => return Err(response_error(e)),
};
let stream = ServerSentEventStream::new(ReceiverStream::new(receiver).map(Event::from_data));
Ok(stream.into_response())
}
pub async fn api_v1_get_workers(auth_data: AuthData, State(state): State<Arc<AxumState>>) -> ApiResult<Vec<WorkerPublicData>> {
response(state.application.get_workers(&auth_data).await)
}
pub async fn api_v1_get_workers_updates(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
) -> Result<Response, (StatusCode, Json<ApiError>)> {
let receiver = match state.application.get_workers_updates(&auth_data).await {
Ok(r) => r,
Err(e) => return Err(response_error(e)),
};
let stream = ServerSentEventStream::new(ReceiverStream::new(receiver).map(Event::from_data));
Ok(stream.into_response())
}
pub async fn api_v1_worker_connect(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
request: Request<Body>,
) -> Result<Response, (StatusCode, Json<ApiError>)> {
let token = auth_data.token.as_ref().ok_or_else(|| response_error(error_unauthorized()))?;
if Some(token.secret.as_str()) != state.application.configuration.self_role.get_worker_token() {
return Err(response_error(error_unauthorized()));
}
let ws_upgrade = WebSocketUpgrade::from_request(request, &state)
.await
.map_err(|e| response_error(e.into()))?;
let worker_id = token.id.clone();
let response = ws_upgrade.on_upgrade(move |socket| worker_connect_handle(socket, state.clone(), worker_id));
Ok(response)
}
async fn worker_connect_handle(web_socket: WebSocket, state: Arc<AxumState>, worker_id: String) {
if let Err(error) = worker_connect_handle_inner(web_socket, state, worker_id).await {
error!("{error}");
if let Some(backtrace) = error.backtrace.as_ref() {
error!("{backtrace}");
}
}
}
const HEARTBEAT_TIMEOUT: u64 = 150;
async fn worker_connect_handle_inner(web_socket: WebSocket, state: Arc<AxumState>, worker_id: String) -> Result<(), ApiError> {
let (mut ws_sender, mut ws_receiver) = web_socket.split();
let Some(Ok(Message::Text(data))) = ws_receiver.next().await else {
ws_sender.send(Message::Close(None)).await?;
return Err(specialize(
error_invalid_request(),
String::from("expected the worker descriptor"),
));
};
let descriptor = serde_json::from_str::<WorkerDescriptor>(&data)?;
if worker_id != descriptor.identifier {
ws_sender.send(Message::Close(None)).await?;
return Err(specialize(error_unauthorized(), String::from("unexpected worker identifier")));
}
let worker_id = descriptor.identifier.clone();
let ws_sender = Arc::new(Mutex::new(ws_sender));
ws_sender
.lock()
.await
.send(Message::Text(serde_json::to_string(
&state.application.configuration.get_self_as_external(),
)?))
.await?;
let (to_health_checker, mut health_checker_receiver) = channel::<Vec<u8>>(8);
let (job_sender, mut job_receiver) = channel::<JobSpecification>(8);
let (updated_sender, update_receiver) = channel::<JobUpdate>(8);
let ws_dispatcher: Pin<Box<dyn Future<Output = Result<(), ApiError>> + Send>> = {
Box::pin(async move {
while let Some(message) = ws_receiver.next().await {
match message? {
Message::Text(data) => {
let update = serde_json::from_str(&data)?;
updated_sender.send(update).await?;
}
Message::Binary(data) => {
let update = serde_json::from_slice(&data)?;
updated_sender.send(update).await?;
}
Message::Ping(_) => { }
Message::Pong(data) => {
to_health_checker.send(data).await?;
}
Message::Close(_) => {
break;
}
}
}
Ok::<_, ApiError>(())
})
};
let health_check = Box::pin(async move {
let mut code: u8 = 0;
loop {
let Some(data) =
tokio::time::timeout(Duration::from_millis(HEARTBEAT_TIMEOUT), health_checker_receiver.recv()).await?
else {
break;
};
if data[0] != code {
return Err(specialize(
error_backend_failure(),
format!("invalid heartbeat, expected {code}, got {}", data[0]),
));
}
code = code.wrapping_add(1);
}
Ok::<_, ApiError>(())
});
let job_bridge = {
let ws_sender = ws_sender.clone();
Box::pin(async move {
while let Some(job) = job_receiver.recv().await {
ws_sender
.lock()
.await
.send(Message::Text(serde_json::to_string(&job)?))
.await?;
}
Ok::<_, ApiError>(())
})
};
state.application.worker_nodes.register_worker(WorkerRegistrationData {
descriptor,
job_sender,
update_receiver,
});
let (result, _index, _rest) = select_all(vec![ws_dispatcher, health_check, job_bridge]).await;
state.application.worker_nodes.remove_worker(&worker_id);
result
}
pub async fn api_v1_get_users(auth_data: AuthData, State(state): State<Arc<AxumState>>) -> ApiResult<Vec<RegistryUser>> {
response(state.application.get_users(&auth_data).await)
}
pub async fn api_v1_update_user(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(Base64(email)): Path<Base64>,
target: Json<RegistryUser>,
) -> ApiResult<RegistryUser> {
if email != target.email {
return Err(response_error(specialize(
error_invalid_request(),
String::from("email in path and body are different"),
)));
}
response(state.application.update_user(&auth_data, &target).await)
}
pub async fn api_v1_delete_user(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(Base64(email)): Path<Base64>,
) -> ApiResult<()> {
response(state.application.delete_user(&auth_data, &email).await)
}
pub async fn api_v1_deactivate_user(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(Base64(email)): Path<Base64>,
) -> ApiResult<()> {
response(state.application.deactivate_user(&auth_data, &email).await)
}
pub async fn api_v1_reactivate_user(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(Base64(email)): Path<Base64>,
) -> ApiResult<()> {
response(state.application.reactivate_user(&auth_data, &email).await)
}
#[derive(Deserialize)]
pub struct SearchForm {
q: String,
per_page: Option<usize>,
deprecated: Option<bool>,
}
pub async fn api_v1_cargo_search(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
form: Query<SearchForm>,
) -> ApiResult<SearchResults> {
response(
state
.application
.search_crates(&auth_data, &form.q, form.per_page, form.deprecated)
.await,
)
}
pub async fn api_v1_get_crates_stats(auth_data: AuthData, State(state): State<Arc<AxumState>>) -> ApiResult<GlobalStats> {
response(state.application.get_crates_stats(&auth_data).await)
}
pub async fn api_v1_get_crates_undocumented(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
) -> ApiResult<Vec<DocGenJobSpec>> {
response(state.application.get_undocumented_crates(&auth_data).await)
}
pub async fn api_v1_get_crates_outdated_heads(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
) -> ApiResult<Vec<CrateVersion>> {
response(state.application.get_crates_outdated_heads(&auth_data).await)
}
pub async fn api_v1_cargo_publish_crate_version(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
body: Bytes,
) -> ApiResult<CrateUploadResult> {
response(state.application.publish_crate_version(&auth_data, &body).await)
}
pub async fn api_v1_get_crate_info(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
) -> ApiResult<CrateInfo> {
response(state.application.get_crate_info(&auth_data, &package).await)
}
pub async fn api_v1_get_crate_last_readme(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 1], Vec<u8>), (StatusCode, Json<ApiError>)> {
let data = state
.application
.get_crate_last_readme(&auth_data, &package)
.await
.map_err(response_error)?;
Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, HeaderValue::from_static("text/markdown"))],
data,
))
}
pub async fn api_v1_get_crate_readme(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrateVersion { package, version }): Path<PathInfoCrateVersion>,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 1], Vec<u8>), (StatusCode, Json<ApiError>)> {
let data = state
.application
.get_crate_readme(&auth_data, &package, &version)
.await
.map_err(response_error)?;
Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, HeaderValue::from_static("text/markdown"))],
data,
))
}
pub async fn api_v1_download_crate(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrateVersion { package, version }): Path<PathInfoCrateVersion>,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 1], Vec<u8>), (StatusCode, Json<ApiError>)> {
match state.application.get_crate_content(&auth_data, &package, &version).await {
Ok(data) => Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"))],
data,
)),
Err(mut error) => {
if error.http == 401 {
error.http = 403;
}
Err(response_error(error))
}
}
}
pub async fn api_v1_cargo_yank(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrateVersion { package, version }): Path<PathInfoCrateVersion>,
) -> ApiResult<YesNoResult> {
response(state.application.yank_crate_version(&auth_data, &package, &version).await)
}
pub async fn api_v1_cargo_unyank(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrateVersion { package, version }): Path<PathInfoCrateVersion>,
) -> ApiResult<YesNoResult> {
response(state.application.unyank_crate_version(&auth_data, &package, &version).await)
}
pub async fn api_v1_regen_crate_version_doc(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrateVersion { package, version }): Path<PathInfoCrateVersion>,
) -> ApiResult<Vec<DocGenJob>> {
response(
state
.application
.regen_crate_version_doc(&auth_data, &package, &version)
.await,
)
}
pub async fn api_v1_check_crate_version(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrateVersion { package, version }): Path<PathInfoCrateVersion>,
) -> ApiResult<DepsAnalysis> {
response(
state
.application
.check_crate_version_deps(&auth_data, &package, &version)
.await,
)
}
pub async fn api_v1_get_crate_dl_stats(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
) -> ApiResult<DownloadStats> {
response(state.application.get_crate_dl_stats(&auth_data, &package).await)
}
pub async fn api_v1_cargo_get_crate_owners(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
) -> ApiResult<OwnersQueryResult> {
response(state.application.get_crate_owners(&auth_data, &package).await)
}
pub async fn api_v1_cargo_add_crate_owners(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
input: Json<OwnersChangeQuery>,
) -> ApiResult<YesNoMsgResult> {
response(state.application.add_crate_owners(&auth_data, &package, &input.users).await)
}
pub async fn api_v1_cargo_remove_crate_owners(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
input: Json<OwnersChangeQuery>,
) -> ApiResult<YesNoResult> {
response(
state
.application
.remove_crate_owners(&auth_data, &package, &input.users)
.await,
)
}
pub async fn api_v1_get_crate_targets(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
) -> ApiResult<Vec<CrateInfoTarget>> {
response(state.application.get_crate_targets(&auth_data, &package).await)
}
pub async fn api_v1_set_crate_targets(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
input: Json<Vec<CrateInfoTarget>>,
) -> ApiResult<()> {
response(state.application.set_crate_targets(&auth_data, &package, &input).await)
}
pub async fn api_v1_get_crate_required_capabilities(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
) -> ApiResult<Vec<String>> {
response(state.application.get_crate_required_capabilities(&auth_data, &package).await)
}
pub async fn api_v1_set_crate_required_capabilities(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
input: Json<Vec<String>>,
) -> ApiResult<()> {
response(
state
.application
.set_crate_required_capabilities(&auth_data, &package, &input)
.await,
)
}
pub async fn api_v1_set_crate_deprecation(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Path(PathInfoCrate { package }): Path<PathInfoCrate>,
input: Json<bool>,
) -> ApiResult<()> {
response(state.application.set_crate_deprecation(&auth_data, &package, input.0).await)
}
pub async fn index_serve_inner(
index: &(dyn Index + Send + Sync),
path: &str,
) -> Result<(impl Stream<Item = Result<impl Into<Bytes>, impl Into<BoxError>>>, HeaderValue), ApiError> {
let file_path: PathBuf = path.parse()?;
let file_path = index.get_index_file(&file_path).await?.ok_or_else(error_not_found)?;
let file = File::open(file_path).await.map_err(|_e| error_not_found())?;
let stream = ReaderStream::new(file);
if std::path::Path::new(path)
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("json"))
{
Ok((stream, HeaderValue::from_static("application/json")))
} else if path == "/HEAD" || path.starts_with("/info") {
Ok((stream, HeaderValue::from_static("text/plain; charset=utf-8")))
} else {
Ok((stream, HeaderValue::from_static("application/octet-stream")))
}
}
fn index_serve_map_err(e: ApiError, domain: &str) -> (StatusCode, [(HeaderName, HeaderValue); 2], Json<ApiError>) {
let (status, body) = response_error(e);
(
status,
[
(
header::WWW_AUTHENTICATE,
HeaderValue::from_str(&format!("Basic realm={domain}")).unwrap(),
),
(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")),
],
body,
)
}
pub async fn index_serve_check_auth(
application: &Application,
auth_data: &AuthData,
) -> Result<(), (StatusCode, [(HeaderName, HeaderValue); 2], Json<ApiError>)> {
application
.authenticate(auth_data)
.await
.map_err(|e| index_serve_map_err(e, &application.configuration.web_domain))?;
Ok(())
}
pub async fn index_serve(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
request: Request<Body>,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 2], Body), (StatusCode, [(HeaderName, HeaderValue); 2], Json<ApiError>)> {
let map_err = |e| index_serve_map_err(e, &state.application.configuration.web_domain);
let path = request.uri().path();
if path != "/config.json" && !state.application.configuration.index.allow_protocol_sparse {
return Err(map_err(error_not_found()));
}
index_serve_check_auth(&state.application, &auth_data).await?;
let (stream, content_type) = index_serve_inner(state.application.get_service_index(), path)
.await
.map_err(map_err)?;
let body = Body::from_stream(stream);
Ok((
StatusCode::OK,
[
(header::CONTENT_TYPE, content_type),
(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")),
],
body,
))
}
#[allow(clippy::implicit_hasher)]
pub async fn index_serve_info_refs(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
Query(query): Query<HashMap<String, String>>,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 2], Body), (StatusCode, [(HeaderName, HeaderValue); 2], Json<ApiError>)> {
let map_err = |e| index_serve_map_err(e, &state.application.configuration.web_domain);
if !state.application.configuration.index.allow_protocol_git {
return Err(map_err(error_not_found()));
}
index_serve_check_auth(&state.application, &auth_data).await?;
if query.get("service").map(String::as_str) == Some("git-upload-pack") {
let data = state
.application
.get_service_index()
.get_upload_pack_info_refs()
.await
.map_err(map_err)?;
Ok((
StatusCode::OK,
[
(
header::CONTENT_TYPE,
HeaderValue::from_static("application/x-git-upload-pack-advertisement"),
),
(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")),
],
Body::from(data),
))
} else {
Err(map_err(error_not_found()))
}
}
pub async fn index_serve_git_upload_pack(
auth_data: AuthData,
State(state): State<Arc<AxumState>>,
body: Bytes,
) -> Result<(StatusCode, [(HeaderName, HeaderValue); 2], Body), (StatusCode, [(HeaderName, HeaderValue); 2], Json<ApiError>)> {
let map_err = |e| index_serve_map_err(e, &state.application.configuration.web_domain);
if !state.application.configuration.index.allow_protocol_git {
return Err(map_err(error_not_found()));
}
index_serve_check_auth(&state.application, &auth_data).await?;
let data = state
.application
.get_service_index()
.get_upload_pack_for(&body)
.await
.map_err(map_err)?;
Ok((
StatusCode::OK,
[
(
header::CONTENT_TYPE,
HeaderValue::from_static("application/x-git-upload-pack-result"),
),
(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")),
],
Body::from(data),
))
}
pub async fn get_version() -> ApiResult<AppVersion> {
response(Ok(AppVersion {
commit: crate::GIT_HASH.to_string(),
tag: crate::GIT_TAG.to_string(),
}))
}