Skip to main content

crates_io/
lib.rs

1//! > This crate is maintained by the Cargo team for use by the wider
2//! > ecosystem. This crate follows semver compatibility for its APIs.
3
4use std::collections::BTreeMap;
5use std::fs::File;
6use std::io::prelude::*;
7use std::io::{Cursor, SeekFrom};
8use std::time::Instant;
9
10use http::{Method, Request, Response, StatusCode};
11use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
12use serde::{Deserialize, Serialize};
13use url::Url;
14
15type RegistryResult<T, E> = Result<T, Error<E>>;
16
17/// Perform an HTTP request and return the response.
18///
19/// Users of this crate must provide an implementation of this
20/// trait using an HTTP crate such as `curl`, `reqwest`, etc.
21pub trait HttpClient {
22    type Error: std::error::Error + Send + Sync;
23    fn request(&self, req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Self::Error>;
24}
25
26pub struct Registry<T: HttpClient> {
27    /// The base URL for issuing API requests.
28    host: String,
29    /// Optional authorization token.
30    /// If None, commands requiring authorization will fail.
31    token: Option<String>,
32    /// HTTP handle for issuing requests.
33    handle: T,
34    /// Whether to include the authorization token with all requests.
35    auth_required: bool,
36}
37
38#[derive(PartialEq, Clone, Copy)]
39pub enum Auth {
40    Authorized,
41    Unauthorized,
42}
43
44#[derive(Deserialize)]
45pub struct Crate {
46    pub name: String,
47    pub description: Option<String>,
48    pub max_version: String,
49}
50
51/// This struct is serialized as JSON and sent as metadata ahead of the crate
52/// tarball when publishing crates to a crate registry like crates.io.
53///
54/// see <https://doc.rust-lang.org/cargo/reference/registry-web-api.html#publish>
55#[derive(Serialize, Deserialize)]
56pub struct NewCrate {
57    pub name: String,
58    pub vers: String,
59    pub deps: Vec<NewCrateDependency>,
60    pub features: BTreeMap<String, Vec<String>>,
61    pub authors: Vec<String>,
62    pub description: Option<String>,
63    pub documentation: Option<String>,
64    pub homepage: Option<String>,
65    pub readme: Option<String>,
66    pub readme_file: Option<String>,
67    pub keywords: Vec<String>,
68    pub categories: Vec<String>,
69    pub license: Option<String>,
70    pub license_file: Option<String>,
71    pub repository: Option<String>,
72    pub badges: BTreeMap<String, BTreeMap<String, String>>,
73    pub links: Option<String>,
74    pub rust_version: Option<String>,
75}
76
77#[derive(Serialize, Deserialize)]
78pub struct NewCrateDependency {
79    pub optional: bool,
80    pub default_features: bool,
81    pub name: String,
82    pub features: Vec<String>,
83    pub version_req: String,
84    pub target: Option<String>,
85    pub kind: String,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub registry: Option<String>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub explicit_name_in_toml: Option<String>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub artifact: Option<Vec<String>>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub bindep_target: Option<String>,
94    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
95    pub lib: bool,
96}
97
98#[derive(Deserialize)]
99pub struct User {
100    pub id: u32,
101    pub login: String,
102    pub avatar: Option<String>,
103    pub email: Option<String>,
104    pub name: Option<String>,
105}
106
107pub struct Warnings {
108    pub invalid_categories: Vec<String>,
109    pub invalid_badges: Vec<String>,
110    pub other: Vec<String>,
111}
112
113#[derive(Deserialize)]
114struct R {
115    ok: bool,
116}
117#[derive(Deserialize)]
118struct OwnerResponse {
119    ok: bool,
120    msg: String,
121}
122#[derive(Deserialize)]
123struct ApiErrorList {
124    errors: Vec<ApiError>,
125}
126#[derive(Deserialize)]
127struct ApiError {
128    detail: String,
129}
130#[derive(Serialize)]
131struct OwnersReq<'a> {
132    users: &'a [&'a str],
133}
134#[derive(Deserialize)]
135struct Users {
136    users: Vec<User>,
137}
138#[derive(Deserialize)]
139struct TotalCrates {
140    total: u32,
141}
142#[derive(Deserialize)]
143struct Crates {
144    crates: Vec<Crate>,
145    meta: TotalCrates,
146}
147
148/// Error returned when interacting with a registry.
149#[derive(Debug, thiserror::Error)]
150#[non_exhaustive]
151pub enum Error<T> {
152    /// Error from underlying transport.
153    #[error(transparent)]
154    Transport(T),
155
156    /// Error from http.
157    #[error(transparent)]
158    Http(#[from] http::Error),
159
160    /// Error from serializing the request payload and deserializing the
161    /// response body (like response body didn't match expected structure).
162    #[error(transparent)]
163    Json(#[from] serde_json::Error),
164
165    /// Error from IO. Mostly from reading the tarball to upload.
166    #[error("failed to seek tarball")]
167    Io(#[from] std::io::Error),
168
169    /// Response body was not valid utf8.
170    #[error("invalid response body from server")]
171    Utf8(#[from] std::string::FromUtf8Error),
172
173    /// Error from API response containing JSON field `errors.details`.
174    #[error(
175        "the remote server responded with an error{}: {}",
176        status(*code),
177        errors.join(", "),
178    )]
179    Api {
180        code: StatusCode,
181        headers: Vec<String>,
182        errors: Vec<String>,
183    },
184
185    /// Error from API response which didn't have pre-programmed `errors.details`.
186    #[error(
187        "failed to get a 200 OK response, got {}\nheaders:\n\t{}\nbody:\n{body}",
188        code.as_u16(),
189        headers.join("\n\t"),
190    )]
191    Code {
192        code: StatusCode,
193        headers: Vec<String>,
194        body: String,
195    },
196
197    #[error(transparent)]
198    InvalidToken(#[from] TokenError),
199
200    /// Server was unavailable and timed out. Happened when uploading a way
201    /// too large tarball to crates.io.
202    #[error(
203        "Request timed out after 30 seconds. If you're trying to \
204         upload a crate it may be too large. If the crate is under \
205         10MB in size, you can email help@crates.io for assistance.\n\
206         Total size was {0}."
207    )]
208    Timeout(u64),
209}
210
211impl<T: HttpClient> Registry<T> {
212    /// Creates a new `Registry`.
213    ///
214    /// ## Example
215    ///
216    /// ```rust
217    /// use crates_io::{Registry, HttpClient};
218    /// use http::{Request, Response};
219    ///
220    /// struct Client {}
221    /// impl HttpClient for Client {
222    ///     type Error = std::io::Error;
223    ///     fn request(&self, req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Self::Error> {
224    ///         todo!()
225    ///     }
226    /// }
227    /// let client = Client {};
228    ///
229    /// let mut reg = Registry::new_handle(String::from("https://crates.io"), None, client, false);
230    /// ```
231    pub fn new_handle(host: String, token: Option<String>, handle: T, auth_required: bool) -> Self {
232        Self {
233            host,
234            token,
235            handle,
236            auth_required,
237        }
238    }
239
240    pub fn set_token(&mut self, token: Option<String>) {
241        self.token = token;
242    }
243
244    fn token(&self) -> RegistryResult<&str, T::Error> {
245        let token = self.token.as_ref().ok_or_else(|| TokenError::Missing)?;
246        check_token(token)?;
247        Ok(token)
248    }
249
250    pub fn host(&self) -> &str {
251        &self.host
252    }
253
254    pub fn host_is_crates_io(&self) -> bool {
255        is_url_crates_io(&self.host)
256    }
257
258    pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> RegistryResult<String, T::Error> {
259        let body = serde_json::to_string(&OwnersReq { users: owners })?;
260        let body = self.put(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
261        assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
262        Ok(serde_json::from_str::<OwnerResponse>(&body)?.msg)
263    }
264
265    pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> RegistryResult<(), T::Error> {
266        let body = serde_json::to_string(&OwnersReq { users: owners })?;
267        let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
268        assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
269        Ok(())
270    }
271
272    pub fn list_owners(&mut self, krate: &str) -> RegistryResult<Vec<User>, T::Error> {
273        let body = self.get(&format!("/crates/{}/owners", krate))?;
274        Ok(serde_json::from_str::<Users>(&body)?.users)
275    }
276
277    pub fn publish(
278        &mut self,
279        krate: &NewCrate,
280        mut tarball: &File,
281    ) -> RegistryResult<Warnings, T::Error> {
282        let json = serde_json::to_string(krate)?;
283        // Prepare the body. The format of the upload request is:
284        //
285        //      <le u32 of json>
286        //      <json request> (metadata for the package)
287        //      <le u32 of tarball>
288        //      <source tarball>
289
290        // NOTE: This can be replaced with `stream_len` if it is ever stabilized.
291        //
292        // This checks the length using seeking instead of metadata, because
293        // on some filesystems, getting the metadata will fail because
294        // the file was renamed in ops::package.
295        let tarball_len = tarball.seek(SeekFrom::End(0))?;
296        tarball.seek(SeekFrom::Start(0))?;
297        let header = {
298            let mut w = Vec::new();
299            w.extend(&(json.len() as u32).to_le_bytes());
300            w.extend(json.as_bytes().iter().cloned());
301            w.extend(&(tarball_len as u32).to_le_bytes());
302            w
303        };
304        let mut body = Vec::new();
305        Cursor::new(header).chain(tarball).read_to_end(&mut body)?;
306        let url = self.api_url("/crates/new");
307
308        let request = http::Request::put(url)
309            .header(http::header::CONTENT_TYPE, "application/octet-stream")
310            .header(http::header::ACCEPT, "application/json")
311            .header(http::header::AUTHORIZATION, self.token()?)
312            .body(body)?;
313        let started = Instant::now();
314        let response = self.handle.request(request).map_err(Error::Transport)?;
315        let body = self.handle(response).map_err(|e| match e {
316            Error::Code { code, .. }
317                if code == StatusCode::SERVICE_UNAVAILABLE
318                    && started.elapsed().as_secs() >= 29
319                    && self.host_is_crates_io() =>
320            {
321                Error::Timeout(tarball_len)
322            }
323            _ => e.into(),
324        })?;
325
326        let response = if body.is_empty() {
327            "{}".parse()?
328        } else {
329            body.parse::<serde_json::Value>()?
330        };
331
332        let invalid_categories: Vec<String> = response
333            .get("warnings")
334            .and_then(|j| j.get("invalid_categories"))
335            .and_then(|j| j.as_array())
336            .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
337            .unwrap_or_else(Vec::new);
338
339        let invalid_badges: Vec<String> = response
340            .get("warnings")
341            .and_then(|j| j.get("invalid_badges"))
342            .and_then(|j| j.as_array())
343            .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
344            .unwrap_or_else(Vec::new);
345
346        let other: Vec<String> = response
347            .get("warnings")
348            .and_then(|j| j.get("other"))
349            .and_then(|j| j.as_array())
350            .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
351            .unwrap_or_else(Vec::new);
352
353        Ok(Warnings {
354            invalid_categories,
355            invalid_badges,
356            other,
357        })
358    }
359
360    pub fn search(
361        &mut self,
362        query: &str,
363        limit: u32,
364    ) -> RegistryResult<(Vec<Crate>, u32), T::Error> {
365        let formatted_query = percent_encode(query.as_bytes(), NON_ALPHANUMERIC);
366        let body = self.req(
367            Method::GET,
368            &format!("/crates?q={}&per_page={}", formatted_query, limit),
369            None,
370            Auth::Unauthorized,
371        )?;
372
373        let crates = serde_json::from_str::<Crates>(&body)?;
374        Ok((crates.crates, crates.meta.total))
375    }
376
377    pub fn yank(&mut self, krate: &str, version: &str) -> RegistryResult<(), T::Error> {
378        let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?;
379        assert!(serde_json::from_str::<R>(&body)?.ok);
380        Ok(())
381    }
382
383    pub fn unyank(&mut self, krate: &str, version: &str) -> RegistryResult<(), T::Error> {
384        let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), None)?;
385        assert!(serde_json::from_str::<R>(&body)?.ok);
386        Ok(())
387    }
388
389    fn put(&mut self, path: &str, b: Option<&[u8]>) -> RegistryResult<String, T::Error> {
390        self.req(Method::PUT, path, b, Auth::Authorized)
391    }
392
393    fn get(&mut self, path: &str) -> RegistryResult<String, T::Error> {
394        self.req(Method::GET, path, None, Auth::Authorized)
395    }
396
397    fn delete(&mut self, path: &str, b: Option<&[u8]>) -> RegistryResult<String, T::Error> {
398        self.req(Method::DELETE, path, b, Auth::Authorized)
399    }
400
401    fn api_url(&self, path: &str) -> String {
402        // http::Uri doesn't support file urls without an authority, even though it's optional.
403        // We insert localhost here to make it work.
404        let host = &self.host;
405        if let Some(file_url) = host.strip_prefix("file:///") {
406            format!("file://localhost/{file_url}/api/v1{path}")
407        } else {
408            format!("{host}/api/v1{path}")
409        }
410    }
411
412    fn req(
413        &mut self,
414        method: Method,
415        path: &str,
416        body: Option<&[u8]>,
417        authorized: Auth,
418    ) -> RegistryResult<String, T::Error> {
419        let url = self.api_url(path);
420        let mut request = http::Request::builder()
421            .method(method)
422            .uri(url)
423            .header(http::header::ACCEPT, "application/json");
424        if body.is_some() {
425            request = request.header(http::header::CONTENT_TYPE, "application/json");
426        }
427
428        if self.auth_required || authorized == Auth::Authorized {
429            request = request.header(http::header::AUTHORIZATION, self.token()?);
430        }
431        let request = request.body(body.unwrap_or_default().to_vec())?;
432        let response = self.handle.request(request).map_err(Error::Transport)?;
433        self.handle(response)
434    }
435
436    fn handle(&mut self, response: http::Response<Vec<u8>>) -> RegistryResult<String, T::Error> {
437        let (head, body) = response.into_parts();
438        let body = String::from_utf8(body)?;
439        let errors = serde_json::from_str::<ApiErrorList>(&body)
440            .ok()
441            .map(|s| s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>());
442
443        let headers = head
444            .headers
445            .iter()
446            .filter_map(|(k, v)| Some((k, v.to_str().ok()?)))
447            .map(|(k, v)| format!("{k}: {v}"))
448            .collect();
449
450        match (head.status, errors) {
451            (code, None) if code.is_success() => Ok(body),
452            (code, Some(errors)) => Err(Error::Api {
453                code,
454                headers,
455                errors,
456            }),
457            (code, None) => Err(Error::Code {
458                code,
459                headers,
460                body,
461            }),
462        }
463    }
464}
465
466fn status(code: StatusCode) -> String {
467    if code.is_success() {
468        String::new()
469    } else {
470        format!(" (status {code})")
471    }
472}
473
474/// Returns `true` if the host of the given URL is "crates.io".
475pub fn is_url_crates_io(url: &str) -> bool {
476    Url::parse(url)
477        .map(|u| u.host_str() == Some("crates.io"))
478        .unwrap_or(false)
479}
480
481#[derive(Debug, thiserror::Error)]
482pub enum TokenError {
483    #[error("no upload token found, please run `cargo login`")]
484    Missing,
485
486    #[error("please provide a non-empty token")]
487    Empty,
488
489    #[error(
490        "token contains invalid characters.\nOnly printable ISO-8859-1 characters \
491             are allowed as it is sent in a HTTPS header."
492    )]
493    InvalidCharacters,
494}
495
496/// Checks if a token is valid or malformed.
497///
498/// This check is necessary to prevent sending tokens which create an invalid HTTP request.
499/// It would be easier to check just for alphanumeric tokens, but we can't be sure that all
500/// registries only create tokens in that format so that is as less restricted as possible.
501pub fn check_token(token: &str) -> Result<(), TokenError> {
502    if token.is_empty() {
503        return Err(TokenError::Empty);
504    }
505    if token.bytes().all(|b| {
506        // This is essentially the US-ASCII limitation of
507        // https://www.rfc-editor.org/rfc/rfc9110#name-field-values. That is,
508        // visible ASCII characters (0x21-0x7e), space, and tab. We want to be
509        // able to pass this in an HTTP header without encoding.
510        b >= 32 && b < 127 || b == b'\t'
511    }) {
512        Ok(())
513    } else {
514        Err(TokenError::InvalidCharacters)
515    }
516}