-
-
Notifications
You must be signed in to change notification settings - Fork 151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
async API #46
async API #46
Conversation
@@ -273,15 +276,18 @@ fn test_exchange_code_successful_with_complete_json_response() { | |||
fn test_exchange_client_credentials_with_basic_auth() { | |||
let mock = mock("POST", "/token") | |||
// base64(urlencode("aaa/;&") + ":" + urlencode("bbb/;&")) | |||
.match_header("Authorization", "Basic YWFhJTJGJTNCJTI2OmJiYiUyRiUzQiUyNg==") | |||
.match_header("Authorization", "basic YWFhJTJGJTNCJTI2OmJiYiUyRiUzQiUyNg==") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reqwest puts lowercase letters.
https://docs.rs/reqwest/0.9.3/src/reqwest/async_impl/request.rs.html#171
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the reference! looking at https://tools.ietf.org/html/rfc7235#section-2.1, the auth scheme is indeed supposed to be case insensitive. hopefully this won't cause any compatibility issues with off-spec server implementations.
I'm thinking of using hyper instead of reqwest. ex. |
thanks for the PR! I'm traveling this weekend, but I'll take a look next week. |
@@ -236,6 +237,24 @@ use prelude::*; | |||
|
|||
const CONTENT_TYPE_JSON: &str = "application/json"; | |||
|
|||
#[derive(Clone)] | |||
/// This struct is used to maintain the strict URI encoding standard as proposed by RFC 3986 | |||
struct StrictEncodeSet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm finding the chain of specs referenced by https://tools.ietf.org/html/rfc6749#appendix-B to be a bit hard to follow, so it's not super clear to me exactly which characters need to be escaped (especially since we're dealing with an HTTP header here, rather than a URI). This code does seem to maintain the current behavior as implemented in https://github.com/curl/curl/blob/master/lib/escape.c#L40, but I wonder if it would suffice to use
USERINFO_ENCODE_SET
, which seems to be intended for username and password escaping (according to https://github.com/dtolnay/url/blob/master/UPGRADING.md).
@DeeUnderscore do you have any thoughts here, as the author of #41? (see usage below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By my reading, this adheres to application/x-www-form-urlencoded
—everything outside of the permitted bytes is encoded. This WHATWG spec seems to be essentially equivalent to what RFC6749 wanted when it described the encoding in Appendix B.
URLs can use forms of percent encoding which aren't application/x-www-form-urlencoded
, since they permit more byte ranges (presumably to make URLs more readable). USERINFO_ENCODE_SET
(WHATWG ref), in particular, is intended for the userinfo component of a URL, ie. the user:password
in https://user:password@example.org/
.
In practical terms, you could probably get away with a less strict encoding, and I suspect most OAuth implementations in the wild avoid using characters which could cause problems anyway. But, for closest adherence to the standard I'd recommend what this PR is doing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the reference and rationale! That makes sense; I agree with the recommendation.
The encoding here does seem to deviate ever so slightly from the WHATWG spec, though:
- spaces encoded with
%20
instead of+
- asterisks (
0x2A
) excluded from the encoding set but mentioned by WHATWG - tildes (
0x7E
) included in encoding set but excluded by WHATWG
None of these deviations seems particularly problematic, though it's probably safer for interoperability to err on the side of percent-encoding more values rather than fewer. Unless there's an error in the WHATWG spec, we should probably remove tildes from this set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable. While the RFC specifically gives spaces as +
in an example, a properly implemented decoder should consume %20
fine, too.
src/lib.rs
Outdated
) -> Result<RequestTokenResponse, curl::Error> { | ||
let mut easy = Easy::new(); | ||
) -> impl Future<Item = RequestTokenResponse, Error = reqwest::Error> + Send + 'static { | ||
let mut req_builder = ::reqwest::async::Client::new().post(&token_url.to_string()[..]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think add hyper::Client
to oauth2::Client
structure as member and create oauth2::Client::with_connector
static method.
Sorry for the delay! Looking this PR over now.
I think it's definitely useful (often required) for clients to be able to specify connection parameters, including TLS settings (trusted CA certs, cipher suites, etc.) and proxy configs. This functionality has been missing from this crate so far, so thank you for bringing it up! However, it doesn't look like As an alternative, it looks like this one refers to a different interface ( I'll submit comments on the rest of the PR soon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again for the PR!
In addition to supporting the async API, I think we should still provide a sync API. That should reduce the boilerplate for callers that want sync behavior, and in particular make this breaking change easier for existing code to adopt.
We probably want both sync and async versions of Client
, similar to what reqwest does. What I'm not sure of is which interface should be the default. We could either have oauth2::Client
be sync and oauth2::async::Client
be async or vice versa (oauth2::Client
is async and oauth2::sync::Client
is sync).
The former is nice because the default interface matches the underlying implementation, while the latter is nice because it avoids making a breaking change altogether (I think). What do you think?
Cargo.toml
Outdated
@@ -18,3 +19,4 @@ url = "1.0" | |||
|
|||
[dev-dependencies] | |||
mockito = "0.8.2" | |||
tokio = "0.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add a newline at the end here
src/lib.rs
Outdated
@@ -211,21 +211,22 @@ | |||
//! | |||
|
|||
extern crate base64; | |||
extern crate curl; | |||
extern crate futures; | |||
extern crate reqwest; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: sort imports
extern crate reqwest; |
src/lib.rs
Outdated
use std::fmt::{Debug, Display, Error as FormatterError, Formatter}; | ||
use std::marker::{Send, Sync, PhantomData}; | ||
use std::ops::Deref; | ||
use std::time::Duration; | ||
|
||
use curl::easy::Easy; | ||
use url::percent_encoding::{utf8_percent_encode, EncodeSet}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use url::percent_encoding::{utf8_percent_encode, EncodeSet}; |
src/lib.rs
Outdated
let content_type = res | ||
.headers() | ||
.get(::reqwest::header::CONTENT_TYPE) | ||
.map(|o| o.to_str().unwrap().to_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a panic is probably undesirable here. it seems like we have a few options:
- return an
Err
if the header contains non-ASCII chars - set
content_type
toNone
in this case - change the type of
RequestTokenResponse::content_type
toOption<reqwest::header::HeaderValue>
I think the last option is probably the cleanest here, but the first seems fine too.
src/lib.rs
Outdated
let token_url = self.token_url.as_ref().unwrap(); | ||
let fut = self.post_request_token(token_url, params).map_err(RequestTokenError::Request) | ||
.and_then(|token_response|{ | ||
Ok(()).and_then(|()| -> Result<_, _> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can simplify this to:
.and_then(|token_response| {
if token_response.http_status != 200 {
....
}
...
})
In other words, the inner closure is already returning a Result
, which transforms automatically into FutureResult
courtesy of impl From<Result<T, E>> for FutureResult<T, E>
. This lets us remove the redundant:
Ok(()).and_then(|| {
...
}).into_future()
@@ -236,6 +237,24 @@ use prelude::*; | |||
|
|||
const CONTENT_TYPE_JSON: &str = "application/json"; | |||
|
|||
#[derive(Clone)] | |||
/// This struct is used to maintain the strict URI encoding standard as proposed by RFC 3986 | |||
struct StrictEncodeSet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm finding the chain of specs referenced by https://tools.ietf.org/html/rfc6749#appendix-B to be a bit hard to follow, so it's not super clear to me exactly which characters need to be escaped (especially since we're dealing with an HTTP header here, rather than a URI). This code does seem to maintain the current behavior as implemented in https://github.com/curl/curl/blob/master/lib/escape.c#L40, but I wonder if it would suffice to use
USERINFO_ENCODE_SET
, which seems to be intended for username and password escaping (according to https://github.com/dtolnay/url/blob/master/UPGRADING.md).
@DeeUnderscore do you have any thoughts here, as the author of #41? (see usage below)
tests/lib.rs
Outdated
@@ -202,8 +203,9 @@ fn test_exchange_code_successful_with_minimal_json_response() { | |||
Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap(), | |||
)), | |||
); | |||
let token = client | |||
.exchange_code(AuthorizationCode::new("ccc".to_string())) | |||
let token = ::tokio::runtime::Runtime::new().unwrap().block_on( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since each test needs to do this, let's define a function that takes a future, creates a runtime, blocks on it, and returns the result. it's probably worth trying to minimize the test boilerplate.
} | ||
|
||
/// async edition | ||
pub fn exchange_code_async( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
futures trait also changes greatly in Rust 2018.
How about doing this as a provisional response for the past few months?
- exchange_code_async
- exchange_password_async
- exchange_client_credentials_async
- exchange_refresh_token_async
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternative choice: just constructs and returns http::request::Request
object.
We can chose own http request library e.g. curl, reqwest, hyper, actix-web ...etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine to put the pre-Rust 2018 async functionality into this crate. However, I think we should do this via a separate Client
implementation (i.e., oauth2::async::Client
). Then, when we're ready to add support for Rust 2018 async, we can call that oauth2::async2018::Client
or something. Each of these can present the same exchange_*
functions, just with different return types. This change will probably require some refactoring to share as much code as possible between the different Client
implementations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, the reason I don't think returning Request
objects would work well is that we'd then have to offer an interface for callers to pass the response back in for us to parse
3146148
to
0755447
Compare
Here are the implementations of https_connector. |
let underscore = byte == 0x5f; | ||
let tilde = byte == 0x7e; | ||
let period = byte == 0x2e; | ||
!(upper || lower || numeric || hyphen || underscore || tilde || period) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
based on the conversation above, I think we should probably remove tilde
} | ||
|
||
/// async edition | ||
pub fn exchange_code_async( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine to put the pre-Rust 2018 async functionality into this crate. However, I think we should do this via a separate Client
implementation (i.e., oauth2::async::Client
). Then, when we're ready to add support for Rust 2018 async, we can call that oauth2::async2018::Client
or something. Each of these can present the same exchange_*
functions, just with different return types. This change will probably require some refactoring to share as much code as possible between the different Client
implementations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there are some merge conflicts now based some other PRs I merged
} | ||
let token_url = self.token_url.as_ref().unwrap(); | ||
let fut = self.post_request_token(token_url, params).map_err(RequestTokenError::Request) | ||
.and_then(|token_response|{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some minor cleanup here to 3ca27ff. Mind pulling in those changes, or giving me permissions to update this PR?
} | ||
|
||
/// async edition | ||
pub fn exchange_code_async( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, the reason I don't think returning Request
objects would work well is that we'd then have to offer an interface for callers to pass the response back in for us to parse
Ohh, I see. So it looks like what we really want to accept is a struct that implements the
|
I would like to use this in the context of actix-web. So I would be very happy if there is a way to specify the client library to use (reqwest, actix, ...). Did you already give any toughts on how this could be done? If it would be desired to have this as a feature I might be able to come up with a PR that allows to select actix-web as a replacement for reqwest. |
I think the simplest approach to supporting multiple HTTP clients is probably via a trait. Since the Alternatively, we could probably do something at compile time based on feature flags, but I haven't looked into that much, and it feels like that would be harder to maintain over time. Was there specific functionality from I think it's also probably worth filing a separate Github issue for this so that we can refocus #44 on the async discussion, and the new issue on the need for a generic HTTP client interface. |
latest master now features an async API with pluggable HTTP clients |
#44 (comment)