Skip to content

Commit 0553c6e

Browse files
committed
Replace curl with reqwest in src/github.rs
This behavior was split into two functions so that one of the 5 places we're calling it from could intercept a 404 response code. Since the happy path otherwise is just calling `.json`, this felt really awkward to me. Instead I've opted to return `NotFound`, and downcast to it in the error path for that one place. I expected to just be able to call `Any::is`, but it turns out this method is an inherent method not a trait method (which makes sense, otherwise `Any` wouldn't be object safe). However, a side effect of that is that even though `Any` is a supertrait of `CargoError`, we can't call `Any::is` since `dyn CargoError` can't be cast to `dyn Any`. This may change at some point in the future (see rust-lang/rfcs#2035), but we have to duplicate the body of `Any::is` for now. Except we can't even just duplicate the body of `Any::is`, because the only trait method for `Any` is unstable so we have to duplicate that method, too.......... Other notable changes: - We're no longer sending `Proxy-Connection: Keep-Alive`. According to Wikipedia, this is "Implemented as a misunderstanding of the HTTP specifications. Common because of mistakes in implementations of early HTTP versions". Firefox recently removed it, as have recent versions of curl. - We will accept gzipped responses now. This is good. - We send an actual user agent, instead of "hello!"
1 parent b969741 commit 0553c6e

15 files changed

+80
-92
lines changed

src/app.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use std::time::Duration;
88
use curl::easy::Easy;
99
use diesel::r2d2;
1010
use git2;
11+
use reqwest;
1112
use oauth2;
1213
use scheduled_thread_pool::ScheduledThreadPool;
1314

1415
use {db, Config, Env};
16+
use util::CargoResult;
1517

1618
/// The `App` struct holds the main components of the application like
1719
/// the database connection pool and configurations
@@ -111,4 +113,17 @@ impl App {
111113
}
112114
handle
113115
}
116+
117+
/// Returns a client for making HTTP requests to upload crate files.
118+
///
119+
/// The handle will go through a proxy if the uploader being used has specified one, which
120+
/// is only done in test mode in order to be able to record and inspect the HTTP requests
121+
/// that tests make.
122+
pub fn http_client(&self) -> CargoResult<reqwest::Client> {
123+
let mut builder = reqwest::Client::builder();
124+
if let Some(proxy) = self.config.uploader.proxy() {
125+
builder = builder.proxy(reqwest::Proxy::all(proxy)?);
126+
}
127+
Ok(builder.build()?)
128+
}
114129
}

src/controllers/user/session.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,7 @@ pub fn github_access_token(req: &mut dyn Request) -> CargoResult<Response> {
100100
.exchange(code.clone())
101101
.map_err(|s| human(&s))?;
102102

103-
let (handle, resp) = github::github(req.app(), "/user", &token)?;
104-
let ghuser: GithubUser = github::parse_github_response(handle, &resp)?;
103+
let ghuser = github::github::<GithubUser>(req.app(), "/user", &token)?;
105104

106105
let user = NewUser::new(
107106
ghuser.id,

src/github.rs

Lines changed: 35 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,57 @@
11
//! This module implements functionality for interacting with GitHub.
22
3-
use curl;
4-
use curl::easy::{Easy, List};
5-
3+
use reqwest::{self, header};
64
use oauth2::*;
75

8-
use serde::Deserialize;
9-
use serde_json;
6+
use serde::de::DeserializeOwned;
107

118
use std::str;
129

1310
use app::App;
14-
use util::{human, internal, CargoResult, ChainError};
11+
use util::{human, internal, CargoResult, CargoError, errors::NotFound};
1512

1613
/// Does all the nonsense for sending a GET to Github. Doesn't handle parsing
1714
/// because custom error-code handling may be desirable. Use
1815
/// `parse_github_response` to handle the "common" processing of responses.
19-
pub fn github(app: &App, url: &str, auth: &Token) -> Result<(Easy, Vec<u8>), curl::Error> {
16+
pub fn github<T>(app: &App, url: &str, auth: &Token) -> CargoResult<T>
17+
where
18+
T: DeserializeOwned,
19+
{
2020
let url = format!("{}://api.github.com{}", app.config.api_protocol, url);
2121
info!("GITHUB HTTP: {}", url);
2222

23-
let mut headers = List::new();
24-
headers
25-
.append("Accept: application/vnd.github.v3+json")
26-
.unwrap();
27-
headers.append("User-Agent: hello!").unwrap();
28-
headers
29-
.append(&format!("Authorization: token {}", auth.access_token))
30-
.unwrap();
31-
32-
let mut handle = app.handle();
33-
handle.url(&url).unwrap();
34-
handle.get(true).unwrap();
35-
handle.http_headers(headers).unwrap();
36-
37-
let mut data = Vec::new();
38-
{
39-
let mut transfer = handle.transfer();
40-
transfer
41-
.write_function(|buf| {
42-
data.extend_from_slice(buf);
43-
Ok(buf.len())
44-
}).unwrap();
45-
transfer.perform()?;
46-
}
47-
Ok((handle, data))
23+
let client = app.http_client()?;
24+
client.get(&url)
25+
.header(header::ACCEPT, "application/vnd.github.v3+json")
26+
.header(header::AUTHORIZATION, format!("token {}", auth.access_token))
27+
.send()?
28+
.error_for_status()
29+
.map_err(handle_error_response)?
30+
.json()
31+
.map_err(Into::into)
4832
}
4933

50-
/// Checks for normal responses
51-
pub fn parse_github_response<'de, 'a: 'de, T: Deserialize<'de>>(
52-
mut resp: Easy,
53-
data: &'a [u8],
54-
) -> CargoResult<T> {
55-
match resp.response_code().unwrap() {
56-
// Ok!
57-
200 => {}
58-
// Unauthorized or Forbidden
59-
401 | 403 => {
60-
return Err(human(
61-
"It looks like you don't have permission \
62-
to query a necessary property from Github \
63-
to complete this request. \
64-
You may need to re-authenticate on \
65-
crates.io to grant permission to read \
66-
github org memberships. Just go to \
67-
https://crates.io/login",
68-
));
69-
}
70-
// Something else
71-
n => {
72-
let resp = String::from_utf8_lossy(data);
73-
return Err(internal(&format_args!(
74-
"didn't get a 200 result from \
75-
github, got {} with: {}",
76-
n, resp
77-
)));
34+
fn handle_error_response(error: reqwest::Error) -> Box<dyn CargoError> {
35+
use reqwest::StatusCode as Status;
36+
37+
match error.status() {
38+
Some(Status::UNAUTHORIZED) | Some(Status::FORBIDDEN) => human(
39+
"It looks like you don't have permission \
40+
to query a necessary property from Github \
41+
to complete this request. \
42+
You may need to re-authenticate on \
43+
crates.io to grant permission to read \
44+
github org memberships. Just go to \
45+
https://crates.io/login",
46+
),
47+
Some(Status::NOT_FOUND) => Box::new(NotFound),
48+
_ => {
49+
internal(&format_args!(
50+
"didn't get a 200 result from github: {}",
51+
error
52+
))
7853
}
7954
}
80-
81-
let json = str::from_utf8(data)
82-
.ok()
83-
.chain_error(|| internal("github didn't send a utf8-response"))?;
84-
85-
serde_json::from_str(json).chain_error(|| internal("github didn't send a valid json response"))
8655
}
8756

8857
/// Gets a token with the given string as the access token, but all

src/models/team.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use diesel::prelude::*;
22

33
use app::App;
44
use github;
5-
use util::{human, CargoResult};
5+
use util::{human, CargoResult, errors::NotFound};
66

77
use models::{Crate, CrateOwner, Owner, OwnerKind, User};
88
use schema::{crate_owners, teams};
@@ -142,8 +142,7 @@ impl Team {
142142
// links. A hundred teams should be enough for any org, right?
143143
let url = format!("/orgs/{}/teams?per_page=100", org_name);
144144
let token = github::token(req_user.gh_access_token.clone());
145-
let (handle, data) = github::github(app, &url, &token)?;
146-
let teams: Vec<GithubTeam> = github::parse_github_response(handle, &data)?;
145+
let teams = github::github::<Vec<GithubTeam>>(app, &url, &token)?;
147146

148147
let team = teams
149148
.into_iter()
@@ -165,8 +164,7 @@ impl Team {
165164
}
166165

167166
let url = format!("/orgs/{}", org_name);
168-
let (handle, resp) = github::github(app, &url, &token)?;
169-
let org: Org = github::parse_github_response(handle, &resp)?;
167+
let org = github::github::<Org>(app, &url, &token)?;
170168

171169
NewTeam::new(&login.to_lowercase(), team.id, team.name, org.avatar_url)
172170
.create_or_update(conn)
@@ -225,14 +223,11 @@ fn team_with_gh_id_contains_user(app: &App, github_id: i32, user: &User) -> Carg
225223

226224
let url = format!("/teams/{}/memberships/{}", &github_id, &user.gh_login);
227225
let token = github::token(user.gh_access_token.clone());
228-
let (mut handle, resp) = github::github(app, &url, &token)?;
229-
230-
// Officially how `false` is returned
231-
if handle.response_code().unwrap() == 404 {
232-
return Ok(false);
233-
}
234-
235-
let membership: Membership = github::parse_github_response(handle, &resp)?;
226+
let membership = match github::github::<Membership>(app, &url, &token) {
227+
// Officially how `false` is returned
228+
Err(ref e) if e.is::<NotFound>() => return Ok(false),
229+
x => x?,
230+
};
236231

237232
// There is also `state: pending` for which we could possibly give
238233
// some feedback, but it's not obvious how that should work.

0 commit comments

Comments
 (0)