use axum::http::StatusCode;
use axum::http::header::AUTHORIZATION;
use serde::Deserialize;
use super::Permission;
use crate::error::Error;
use crate::namespace::Namespace;
#[derive(Deserialize)]
struct Repository {
permissions: Option<Permissions>,
}
#[derive(Deserialize)]
struct Permissions {
#[serde(default)]
push: bool,
#[serde(default)]
admin: bool,
}
#[derive(Deserialize)]
struct User {
login: String,
}
pub async fn permission(
client: &reqwest::Client,
api_url: &str,
token: &str,
ns: &Namespace,
) -> Result<Permission, Error> {
let url = format!("{api_url}/repos/{ns}");
let repository = send(client, &url, token)
.await?
.json::<Repository>()
.await
.map_err(|error| {
tracing::warn!(%error, %url, "forge response could not be parsed");
Error::Forge
})?;
match repository.permissions {
Some(Permissions { admin: true, .. }) => Ok(Permission::Admin),
Some(Permissions { push: true, .. }) => Ok(Permission::Write),
_ => Ok(Permission::Read),
}
}
pub async fn public(
client: &reqwest::Client,
api_url: &str,
ns: &Namespace,
) -> Result<Permission, Error> {
let url = format!("{api_url}/repos/{ns}");
let response = client.get(&url).send().await.map_err(|error| {
tracing::warn!(%error, %url, "forge request failed");
Error::Forge
})?;
if response.status() == StatusCode::OK {
return Ok(Permission::Read);
}
if let Some(retry_after) = throttled(&response) {
tracing::warn!(%url, retry_after, "forge is rate-limiting this server");
return Err(Error::RateLimited { retry_after });
}
match response.status() {
StatusCode::NOT_FOUND => {
tracing::info!(%url, "the forge will not admit this repository anonymously");
Err(Error::Unauthenticated)
}
StatusCode::UNAUTHORIZED => {
tracing::warn!(%url, "the forge refused this server's anonymous lookup");
Err(Error::Unauthenticated)
}
status => {
tracing::warn!(%status, %url, "unexpected forge response to an anonymous lookup");
Err(Error::Unauthenticated)
}
}
}
pub async fn login(client: &reqwest::Client, api_url: &str, token: &str) -> Result<String, Error> {
let url = format!("{api_url}/user");
send(client, &url, token)
.await?
.json::<User>()
.await
.map(|user| user.login)
.map_err(|error| {
tracing::warn!(%error, %url, "forge response could not be parsed");
Error::Forge
})
}
async fn send(
client: &reqwest::Client,
url: &str,
token: &str,
) -> Result<reqwest::Response, Error> {
let response = client
.get(url)
.header(AUTHORIZATION, format!("token {token}"))
.send()
.await
.map_err(|error| {
tracing::warn!(%error, %url, "forge request failed");
Error::Forge
})?;
if response.status() == StatusCode::OK {
return Ok(response);
}
if let Some(retry_after) = throttled(&response) {
tracing::warn!(%url, retry_after, "forge is rate-limiting this server");
return Err(Error::RateLimited { retry_after });
}
match response.status() {
StatusCode::UNAUTHORIZED => Err(Error::Unauthenticated),
StatusCode::FORBIDDEN | StatusCode::NOT_FOUND => {
tracing::info!(%url, "the forge will not admit this repository to this token");
Err(Error::Forbidden)
}
status => {
tracing::warn!(%status, %url, "unexpected forge response");
Err(Error::Forge)
}
}
}
fn throttled(response: &reqwest::Response) -> Option<u64> {
let says_when = response.headers().contains_key("retry-after");
match response.status() {
StatusCode::TOO_MANY_REQUESTS => Some(super::backoff::retry_after(response)),
StatusCode::SERVICE_UNAVAILABLE if says_when => Some(super::backoff::retry_after(response)),
_ => None,
}
}
#[cfg(test)]
mod tests;