freighter_server/
api.rs

1use crate::ServiceState;
2use anyhow::Context;
3use axum::body::Bytes;
4use axum::extract::{Path, Query, State};
5use axum::http::{HeaderMap, StatusCode};
6use axum::routing::{delete, get, post, put};
7use axum::{Form, Json, Router};
8use freighter_api_types::auth::request::AuthForm;
9use freighter_api_types::index::request::{Publish, SearchQuery};
10use freighter_api_types::index::response::{CompletedPublication, SearchResults};
11use freighter_api_types::index::{IndexError, IndexProvider};
12use freighter_api_types::storage::{StorageError, StorageProvider};
13use freighter_auth::{AuthError, AuthProvider};
14use metrics::counter;
15use semver::Version;
16use serde::Deserialize;
17use sha2::{Digest, Sha256};
18use std::sync::Arc;
19
20#[non_exhaustive]
21#[derive(Deserialize)]
22pub struct OwnerListChange {
23    pub users: Vec<String>,
24}
25
26pub fn api_router<I, S, A>() -> Router<Arc<ServiceState<I, S, A>>>
27where
28    I: IndexProvider + Send + Sync + 'static,
29    S: StorageProvider + Send + Sync + Clone + 'static,
30    A: AuthProvider + Send + Sync + 'static,
31{
32    Router::new()
33        .route("/new", put(publish))
34        .route("/:crate_name/:version/yank", delete(yank))
35        .route("/:crate_name/:version/unyank", put(unyank))
36        .route("/:crate_name/owners", get(list_owners))
37        .route("/:crate_name/owners", delete(remove_owners))
38        .route("/:crate_name/owners", put(add_owners))
39        .route("/account", post(register))
40        .route("/", get(search))
41        .fallback(handle_api_fallback)
42}
43
44async fn publish<I, S, A>(
45    headers: HeaderMap,
46    State(state): State<Arc<ServiceState<I, S, A>>>,
47    mut body: Bytes,
48) -> axum::response::Result<Json<CompletedPublication>>
49where
50    I: IndexProvider + Send + Sync,
51    S: StorageProvider + Send + Sync + Clone + 'static,
52    A: AuthProvider,
53{
54    let auth = state.auth.token_from_headers(&headers)?
55        .ok_or((StatusCode::UNAUTHORIZED, "Auth token missing"))?;
56
57    if body.len() <= 4 {
58        return Err((StatusCode::BAD_REQUEST, "Missing body").into());
59    }
60
61    let json_len_bytes = body.split_to(4);
62    let json_len = u32::from_le_bytes(json_len_bytes.as_ref().try_into().unwrap()) as usize;
63
64    if body.len() < json_len {
65        return Err(StatusCode::BAD_REQUEST.into());
66    }
67
68    let json_bytes = body.split_to(json_len);
69
70    if body.len() <= 4 {
71        return Err(StatusCode::BAD_REQUEST.into());
72    }
73
74    let crate_len_bytes = body.split_to(4);
75    let crate_len = u32::from_le_bytes(crate_len_bytes.as_ref().try_into().unwrap()) as usize;
76
77    if body.len() < crate_len {
78        return Err((StatusCode::BAD_REQUEST, "Crate data truncated").into());
79    }
80
81    let crate_bytes = body.split_to(crate_len);
82
83    let json: Publish = serde_json::from_slice(&json_bytes)
84        .map_err(|_| (StatusCode::BAD_REQUEST, "JSON parsing error"))?;
85
86    let auth_result = state.auth.publish(auth, &json.name).await;
87
88    if let Err(e) = &auth_result {
89        let error_label = match e {
90            AuthError::Unauthorized => "unauthorized",
91            AuthError::Forbidden => "forbidden",
92            AuthError::InvalidCredentials => "invalid_credentials",
93            AuthError::Unimplemented => "unimplemented",
94            AuthError::CrateNotFound => "crate_not_found",
95            AuthError::ServiceError(_) => "service_error",
96        };
97
98        counter!("freighter_publish_auth_errors_total", "error" => error_label).increment(1);
99    }
100
101    auth_result?;
102
103    let version = json.vers.to_string();
104    let storage = state.storage.clone();
105    let mut stored_crate = false;
106
107    let res = {
108        let sha256 = Sha256::digest(&crate_bytes);
109        let hash = format!("{sha256:x}");
110        let end_step = std::pin::pin!(async {
111            let res = storage
112                .put_crate(&json.name, &version, crate_bytes, sha256.into())
113                .await;
114
115            if let Err(e) = &res {
116                let error_label = match e {
117                    StorageError::NotFound => "not_found",
118                    StorageError::ServiceError(_) => "service_error",
119                };
120
121                counter!("freighter_publish_tarballs_errors_total", "error" => error_label)
122                    .increment(1);
123            }
124
125            res.context("Failed to store the crate in a storage medium")?;
126
127            stored_crate = true;
128            Ok(())
129        });
130        state.index.publish(&json, &hash, end_step).await
131    };
132
133    match res {
134        Ok(res) => {
135            // publish() is never allowed to proceed without the end_step succeeding.
136            assert!(stored_crate);
137            Ok(Json(res))
138        }
139        Err(e) => {
140            let error_label = match &e {
141                IndexError::Conflict(_) => "conflict",
142                IndexError::CrateNameNotAllowed => "crate_name_not_allowed",
143                IndexError::NotFound => "crate_not_found",
144                IndexError::ServiceError(_) => "service_error",
145            };
146
147            counter!("freighter_publish_index_errors_total", "error" => error_label).increment(1);
148
149            if stored_crate {
150                let _ = storage.delete_crate(&json.name, &version).await;
151            }
152            Err(e.into())
153        }
154    }
155}
156
157async fn yank<I, S, A>(
158    headers: HeaderMap,
159    State(state): State<Arc<ServiceState<I, S, A>>>,
160    Path((name, version)): Path<(String, Version)>,
161) -> axum::response::Result<()>
162where
163    I: IndexProvider,
164    A: AuthProvider,
165{
166    let auth = state.auth.token_from_headers(&headers)?
167        .ok_or((StatusCode::UNAUTHORIZED, "Auth token missing"))?;
168
169    state.auth.auth_yank(auth, &name).await?;
170
171    state.index.yank_crate(&name, &version).await?;
172
173    Ok(())
174}
175
176async fn unyank<I, S, A>(
177    headers: HeaderMap,
178    State(state): State<Arc<ServiceState<I, S, A>>>,
179    Path((name, version)): Path<(String, Version)>,
180) -> axum::response::Result<()>
181where
182    I: IndexProvider,
183    A: AuthProvider,
184{
185    let auth = state.auth.token_from_headers(&headers)?
186        .ok_or((StatusCode::UNAUTHORIZED, "Auth token missing"))?;
187
188    state.auth.auth_yank(auth, &name).await?;
189
190    state.index.unyank_crate(&name, &version).await?;
191
192    Ok(())
193}
194
195async fn list_owners<I, S, A>(
196    headers: HeaderMap,
197    State(state): State<Arc<ServiceState<I, S, A>>>,
198    Path(name): Path<String>,
199) -> axum::response::Result<()>
200where
201    A: AuthProvider,
202{
203    let auth = state.auth.token_from_headers(&headers)?
204        .ok_or((StatusCode::UNAUTHORIZED, "Auth token missing"))?;
205
206    state.auth.list_owners(auth, &name).await?;
207
208    Ok(())
209}
210
211async fn add_owners<I, S, A>(
212    headers: HeaderMap,
213    State(state): State<Arc<ServiceState<I, S, A>>>,
214    Path(name): Path<String>,
215    Json(owners): Json<OwnerListChange>,
216) -> axum::response::Result<()>
217where
218    A: AuthProvider,
219{
220    let auth = state.auth.token_from_headers(&headers)?
221        .ok_or((StatusCode::UNAUTHORIZED, "Auth token missing"))?;
222
223    state
224        .auth
225        .add_owners(
226            auth,
227            &owners.users.iter().map(|x| x.as_str()).collect::<Vec<_>>(),
228            &name,
229        )
230        .await?;
231
232    Ok(())
233}
234
235async fn remove_owners<I, S, A>(
236    headers: HeaderMap,
237    State(state): State<Arc<ServiceState<I, S, A>>>,
238    Path(name): Path<String>,
239    Json(owners): Json<OwnerListChange>,
240) -> axum::response::Result<()>
241where
242    A: AuthProvider,
243{
244    let auth = state.auth.token_from_headers(&headers)?
245        .ok_or((StatusCode::UNAUTHORIZED, "Auth token missing"))?;
246
247    state
248        .auth
249        .remove_owners(
250            auth,
251            &owners.users.iter().map(|x| x.as_str()).collect::<Vec<_>>(),
252            &name,
253        )
254        .await?;
255
256    Ok(())
257}
258
259async fn register<I, S, A>(
260    State(state): State<Arc<ServiceState<I, S, A>>>,
261    Form(auth): Form<AuthForm>,
262) -> axum::response::Result<String>
263where
264    A: AuthProvider,
265{
266    if !state.config.allow_registration {
267        return Err((StatusCode::UNAUTHORIZED, "Registration disabled").into());
268    }
269
270    let token = state.auth.register(&auth.username).await?;
271    Ok(token)
272}
273
274async fn search<I, S, A>(
275    headers: HeaderMap,
276    State(state): State<Arc<ServiceState<I, S, A>>>,
277    Query(query): Query<SearchQuery>,
278) -> axum::response::Result<Json<SearchResults>>
279where
280    I: IndexProvider,
281    A: AuthProvider + Sync,
282{
283    if state.config.auth_required {
284        let token = state.auth.token_from_headers(&headers)?
285            .ok_or((StatusCode::UNAUTHORIZED, "Auth token missing"))?;
286
287        state.auth.auth_view_full_index(token).await?;
288    }
289
290    let search_results = state
291        .index
292        .search(&query.q, query.per_page.map_or(10, |x| x.max(100)))
293        .await?;
294
295    Ok(Json(search_results))
296}
297
298async fn handle_api_fallback() -> (StatusCode, &'static str) {
299    (
300        StatusCode::NOT_FOUND,
301        "Freighter: Invalid URL for the crates.io API endpoint",
302    )
303}