3 use std
::collections
::BTreeMap
;
5 use std
::io
::prelude
::*;
6 use std
::io
::{Cursor, SeekFrom}
;
7 use std
::time
::Instant
;
9 use curl
::easy
::{Easy, List}
;
10 use percent_encoding
::{percent_encode, NON_ALPHANUMERIC}
;
11 use serde
::{Deserialize, Serialize}
;
14 pub type Result
<T
> = std
::result
::Result
<T
, Error
>;
17 /// The base URL for issuing API requests.
19 /// Optional authorization token.
20 /// If None, commands requiring authorization will fail.
21 token
: Option
<String
>,
22 /// Curl handle for issuing requests.
24 /// Whether to include the authorization token with all requests.
28 #[derive(PartialEq, Clone, Copy)]
34 #[derive(Deserialize)]
37 pub description
: Option
<String
>,
38 pub max_version
: String
,
41 #[derive(Serialize, Deserialize)]
45 pub deps
: Vec
<NewCrateDependency
>,
46 pub features
: BTreeMap
<String
, Vec
<String
>>,
47 pub authors
: Vec
<String
>,
48 pub description
: Option
<String
>,
49 pub documentation
: Option
<String
>,
50 pub homepage
: Option
<String
>,
51 pub readme
: Option
<String
>,
52 pub readme_file
: Option
<String
>,
53 pub keywords
: Vec
<String
>,
54 pub categories
: Vec
<String
>,
55 pub license
: Option
<String
>,
56 pub license_file
: Option
<String
>,
57 pub repository
: Option
<String
>,
58 pub badges
: BTreeMap
<String
, BTreeMap
<String
, String
>>,
59 pub links
: Option
<String
>,
60 pub rust_version
: Option
<String
>,
63 #[derive(Serialize, Deserialize)]
64 pub struct NewCrateDependency
{
66 pub default_features
: bool
,
68 pub features
: Vec
<String
>,
69 pub version_req
: String
,
70 pub target
: Option
<String
>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub registry
: Option
<String
>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub explicit_name_in_toml
: Option
<String
>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub artifact
: Option
<Vec
<String
>>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub bindep_target
: Option
<String
>,
80 #[serde(default, skip_serializing_if = "is_false")]
84 fn is_false(x
: &bool
) -> bool
{
88 #[derive(Deserialize)]
92 pub avatar
: Option
<String
>,
93 pub email
: Option
<String
>,
94 pub name
: Option
<String
>,
98 pub invalid_categories
: Vec
<String
>,
99 pub invalid_badges
: Vec
<String
>,
100 pub other
: Vec
<String
>,
103 #[derive(Deserialize)]
107 #[derive(Deserialize)]
108 struct OwnerResponse
{
112 #[derive(Deserialize)]
113 struct ApiErrorList
{
114 errors
: Vec
<ApiError
>,
116 #[derive(Deserialize)]
121 struct OwnersReq
<'a
> {
122 users
: &'a
[&'a
str],
124 #[derive(Deserialize)]
128 #[derive(Deserialize)]
132 #[derive(Deserialize)]
138 /// Error returned when interacting with a registry.
139 #[derive(Debug, thiserror::Error)]
141 /// Error from libcurl.
142 #[error(transparent)]
143 Curl(#[from] curl::Error),
145 /// Error from seriailzing the request payload and deserializing the
146 /// response body (like response body didn't match expected structure).
147 #[error(transparent)]
148 Json(#[from] serde_json::Error),
150 /// Error from IO. Mostly from reading the tarball to upload.
151 #[error("failed to seek tarball")]
152 Io(#[from] std::io::Error),
154 /// Response body was not valid utf8.
155 #[error("invalid response body from server")]
156 Utf8(#[from] std::string::FromUtf8Error),
158 /// Error from API response containing JSON field `errors.details`.
160 "the remote server responded with an error{}: {}",
166 headers
: Vec
<String
>,
170 /// Error from API response which didn't have pre-programmed `errors.details`.
172 "failed to get a 200 OK response, got {code}\nheaders:\n\t{}\nbody:\n{body}",
173 headers
.join("\n\t"),
177 headers
: Vec
<String
>,
181 /// Reason why the token was invalid.
183 InvalidToken(&'
static str),
185 /// Server was unavailable and timeouted. Happened when uploading a way
186 /// too large tarball to crates.io.
188 "Request timed out after 30 seconds. If you're trying to \
189 upload a crate it may be too large. If the crate is under \
190 10MB in size, you can email help@crates.io for assistance.\n\
197 /// Creates a new `Registry`.
202 /// use curl::easy::Easy;
203 /// use crates_io::Registry;
205 /// let mut handle = Easy::new();
206 /// // If connecting to crates.io, a user-agent is required.
207 /// handle.useragent("my_crawler (example.com/info)");
208 /// let mut reg = Registry::new_handle(String::from("https://crates.io"), None, handle, true);
212 token
: Option
<String
>,
224 pub fn set_token(&mut self, token
: Option
<String
>) {
228 fn token(&self) -> Result
<&str> {
229 let token
= self.token
.as_ref().ok_or_else(|| {
230 Error
::InvalidToken("no upload token found, please run `cargo login`")
236 pub fn host(&self) -> &str {
240 pub fn host_is_crates_io(&self) -> bool
{
241 is_url_crates_io(&self.host
)
244 pub fn add_owners(&mut self, krate
: &str, owners
: &[&str]) -> Result
<String
> {
245 let body
= serde_json
::to_string(&OwnersReq { users: owners }
)?
;
246 let body
= self.put(&format
!("/crates/{}/owners", krate
), body
.as_bytes())?
;
247 assert
!(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.ok
);
248 Ok(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.msg
)
251 pub fn remove_owners(&mut self, krate
: &str, owners
: &[&str]) -> Result
<()> {
252 let body
= serde_json
::to_string(&OwnersReq { users: owners }
)?
;
253 let body
= self.delete(&format
!("/crates/{}/owners", krate
), Some(body
.as_bytes()))?
;
254 assert
!(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.ok
);
258 pub fn list_owners(&mut self, krate
: &str) -> Result
<Vec
<User
>> {
259 let body
= self.get(&format
!("/crates/{}/owners", krate
))?
;
260 Ok(serde_json
::from_str
::<Users
>(&body
)?
.users
)
263 pub fn publish(&mut self, krate
: &NewCrate
, mut tarball
: &File
) -> Result
<Warnings
> {
264 let json
= serde_json
::to_string(krate
)?
;
265 // Prepare the body. The format of the upload request is:
268 // <json request> (metadata for the package)
269 // <le u32 of tarball>
272 // NOTE: This can be replaced with `stream_len` if it is ever stabilized.
274 // This checks the length using seeking instead of metadata, because
275 // on some filesystems, getting the metadata will fail because
276 // the file was renamed in ops::package.
277 let tarball_len
= tarball
.seek(SeekFrom
::End(0))?
;
278 tarball
.seek(SeekFrom
::Start(0))?
;
280 let mut w
= Vec
::new();
281 w
.extend(&(json
.len() as u32).to_le_bytes());
282 w
.extend(json
.as_bytes().iter().cloned());
283 w
.extend(&(tarball_len
as u32).to_le_bytes());
286 let size
= tarball_len
as usize + header
.len();
287 let mut body
= Cursor
::new(header
).chain(tarball
);
289 let url
= format
!("{}/api/v1/crates/new", self.host
);
291 self.handle
.put(true)?
;
292 self.handle
.url(&url
)?
;
293 self.handle
.in_filesize(size
as u64)?
;
294 let mut headers
= List
::new();
295 headers
.append("Accept: application/json")?
;
296 headers
.append(&format
!("Authorization: {}", self.token()?
))?
;
297 self.handle
.http_headers(headers
)?
;
299 let started
= Instant
::now();
301 .handle(&mut |buf
| body
.read(buf
).unwrap_or(0))
302 .map_err(|e
| match e
{
303 Error
::Code { code, .. }
305 && started
.elapsed().as_secs() >= 29
306 && self.host_is_crates_io() =>
308 Error
::Timeout(tarball_len
)
313 let response
= if body
.is_empty() {
316 body
.parse
::<serde_json
::Value
>()?
319 let invalid_categories
: Vec
<String
> = response
321 .and_then(|j
| j
.get("invalid_categories"))
322 .and_then(|j
| j
.as_array())
323 .map(|x
| x
.iter().flat_map(|j
| j
.as_str()).map(Into
::into
).collect())
324 .unwrap_or_else(Vec
::new
);
326 let invalid_badges
: Vec
<String
> = response
328 .and_then(|j
| j
.get("invalid_badges"))
329 .and_then(|j
| j
.as_array())
330 .map(|x
| x
.iter().flat_map(|j
| j
.as_str()).map(Into
::into
).collect())
331 .unwrap_or_else(Vec
::new
);
333 let other
: Vec
<String
> = response
335 .and_then(|j
| j
.get("other"))
336 .and_then(|j
| j
.as_array())
337 .map(|x
| x
.iter().flat_map(|j
| j
.as_str()).map(Into
::into
).collect())
338 .unwrap_or_else(Vec
::new
);
347 pub fn search(&mut self, query
: &str, limit
: u32) -> Result
<(Vec
<Crate
>, u32)> {
348 let formatted_query
= percent_encode(query
.as_bytes(), NON_ALPHANUMERIC
);
350 &format
!("/crates?q={}&per_page={}", formatted_query
, limit
),
355 let crates
= serde_json
::from_str
::<Crates
>(&body
)?
;
356 Ok((crates
.crates
, crates
.meta
.total
))
359 pub fn yank(&mut self, krate
: &str, version
: &str) -> Result
<()> {
360 let body
= self.delete(&format
!("/crates/{}/{}/yank", krate
, version
), None
)?
;
361 assert
!(serde_json
::from_str
::<R
>(&body
)?
.ok
);
365 pub fn unyank(&mut self, krate
: &str, version
: &str) -> Result
<()> {
366 let body
= self.put(&format
!("/crates/{}/{}/unyank", krate
, version
), &[])?
;
367 assert
!(serde_json
::from_str
::<R
>(&body
)?
.ok
);
371 fn put(&mut self, path
: &str, b
: &[u8]) -> Result
<String
> {
372 self.handle
.put(true)?
;
373 self.req(path
, Some(b
), Auth
::Authorized
)
376 fn get(&mut self, path
: &str) -> Result
<String
> {
377 self.handle
.get(true)?
;
378 self.req(path
, None
, Auth
::Authorized
)
381 fn delete(&mut self, path
: &str, b
: Option
<&[u8]>) -> Result
<String
> {
382 self.handle
.custom_request("DELETE")?
;
383 self.req(path
, b
, Auth
::Authorized
)
386 fn req(&mut self, path
: &str, body
: Option
<&[u8]>, authorized
: Auth
) -> Result
<String
> {
387 self.handle
.url(&format
!("{}/api/v1{}", self.host
, path
))?
;
388 let mut headers
= List
::new();
389 headers
.append("Accept: application/json")?
;
390 headers
.append("Content-Type: application/json")?
;
392 if self.auth_required
|| authorized
== Auth
::Authorized
{
393 headers
.append(&format
!("Authorization: {}", self.token()?
))?
;
395 self.handle
.http_headers(headers
)?
;
398 self.handle
.upload(true)?
;
399 self.handle
.in_filesize(body
.len() as u64)?
;
400 self.handle(&mut |buf
| body
.read(buf
).unwrap_or(0))
401 .map_err(|e
| e
.into())
403 None
=> self.handle(&mut |_
| 0).map_err(|e
| e
.into()),
407 fn handle(&mut self, read
: &mut dyn FnMut(&mut [u8]) -> usize) -> Result
<String
> {
408 let mut headers
= Vec
::new();
409 let mut body
= Vec
::new();
411 let mut handle
= self.handle
.transfer();
412 handle
.read_function(|buf
| Ok(read(buf
)))?
;
413 handle
.write_function(|data
| {
414 body
.extend_from_slice(data
);
417 handle
.header_function(|data
| {
418 // Headers contain trailing \r\n, trim them to make it easier
420 let s
= String
::from_utf8_lossy(data
).trim().to_string();
421 // Don't let server sneak extra lines anywhere.
422 if s
.contains('
\n'
) {
431 let body
= String
::from_utf8(body
)?
;
432 let errors
= serde_json
::from_str
::<ApiErrorList
>(&body
)
434 .map(|s
| s
.errors
.into_iter().map(|s
| s
.detail
).collect
::<Vec
<_
>>());
436 match (self.handle
.response_code()?
, errors
) {
437 (0, None
) | (200, None
) => Ok(body
),
438 (code
, Some(errors
)) => Err(Error
::Api
{
443 (code
, None
) => Err(Error
::Code
{
452 fn status(code
: u32) -> String
{
456 let reason
= reason(code
);
457 format
!(" (status {code} {reason})")
461 fn reason(code
: u32) -> &'
static str {
462 // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
465 101 => "Switching Protocol",
466 103 => "Early Hints",
470 203 => "Non-Authoritative Information",
472 205 => "Reset Content",
473 206 => "Partial Content",
474 300 => "Multiple Choice",
475 301 => "Moved Permanently",
478 304 => "Not Modified",
479 307 => "Temporary Redirect",
480 308 => "Permanent Redirect",
481 400 => "Bad Request",
482 401 => "Unauthorized",
483 402 => "Payment Required",
486 405 => "Method Not Allowed",
487 406 => "Not Acceptable",
488 407 => "Proxy Authentication Required",
489 408 => "Request Timeout",
492 411 => "Length Required",
493 412 => "Precondition Failed",
494 413 => "Payload Too Large",
495 414 => "URI Too Long",
496 415 => "Unsupported Media Type",
497 416 => "Request Range Not Satisfiable",
498 417 => "Expectation Failed",
499 429 => "Too Many Requests",
500 431 => "Request Header Fields Too Large",
501 500 => "Internal Server Error",
502 501 => "Not Implemented",
503 502 => "Bad Gateway",
504 503 => "Service Unavailable",
505 504 => "Gateway Timeout",
510 /// Returns `true` if the host of the given URL is "crates.io".
511 pub fn is_url_crates_io(url
: &str) -> bool
{
513 .map(|u
| u
.host_str() == Some("crates.io"))
517 /// Checks if a token is valid or malformed.
519 /// This check is necessary to prevent sending tokens which create an invalid HTTP request.
520 /// It would be easier to check just for alphanumeric tokens, but we can't be sure that all
521 /// registries only create tokens in that format so that is as less restricted as possible.
522 pub fn check_token(token
: &str) -> Result
<()> {
523 if token
.is_empty() {
524 return Err(Error
::InvalidToken("please provide a non-empty token"));
526 if token
.bytes().all(|b
| {
527 // This is essentially the US-ASCII limitation of
528 // https://www.rfc-editor.org/rfc/rfc9110#name-field-values. That is,
529 // visible ASCII characters (0x21-0x7e), space, and tab. We want to be
530 // able to pass this in an HTTP header without encoding.
531 b
>= 32 && b
< 127 || b
== b'
\t'
535 Err(Error
::InvalidToken(
536 "token contains invalid characters.\nOnly printable ISO-8859-1 characters \
537 are allowed as it is sent in a HTTPS header.",