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 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}