-
Notifications
You must be signed in to change notification settings - Fork 203
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
Add TLS support #81
Add TLS support #81
Conversation
When `-- features tls` is specified for tarpc, RPC communication can also occur over a `TlsStream<TcpStream>` instead of a `TcpStream`. The functional tests have been refactored to use a common set of functions for constructing the client and server structs so that all the tests are shared across non-tls and tls test runs.
A new version of `native-tls` has been released so we don’t need to depend on the git version anymore.
e0f27cb
to
c5a5dd6
Compare
Thanks again for doing this! Hopefully I'll get through it all by the end of the weekend. |
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 haven't looked at the tests yet, but everything else looks great. Thank you so much.
I'd like @shaladdle to do a once-over, as well.
@@ -21,7 +21,8 @@ before_script: | |||
|
|||
script: | |||
- | | |||
travis-cargo build && travis-cargo test | |||
travis-cargo build && travis-cargo test && | |||
travis-cargo build -- --features tls && travis-cargo test -- --features tls | |||
|
|||
after_success: | |||
- travis-cargo coveralls --no-sudo |
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.
How does this work with features?
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.
These run just like cargo build --features tls
. If you pass --
to travis-cargo
then whatever follows is passed to cargo
.
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.
What about coveralls? Looks like it needs to have the tls feature enabled.
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.
Ideally yes... However, coveralls is broken already in travis-cargo
. I think there are a few PRs to fix it in their repo. Maybe we should have a separate PR just to remove travis-cargo
. It may be easier than using that tool.
README.md
Outdated
let (acceptor, client_cx) = tls_context(); | ||
HelloServer.listen(addr, server::Options::default() | ||
.handle(core.handle()) | ||
.tls(acceptor)).wait().unwrap(); |
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.
Building the client_cx seems pretty straightforward; maybe we could just inline it as .tls(TlsClientContext::new("foobar.com"))
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.
Good idea!
src/client.rs
Outdated
|
||
/// Provides the connection Fn impl for Tls | ||
#[doc(hidden)] | ||
pub struct ConnectFn { |
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 could be mistaken, but I have a feeling this can be private?
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.
Oh yes, awesome!
Rust was originally complaining about private type in a public interface, but that was with an older version of the Connect
trait. Making this private seems okay now. I like that it can be private again as well.
src/server.rs
Outdated
inner: future::Either::B(future::ok(listen_with(new_service, | ||
addr, | ||
handle, | ||
acceptor))), | ||
} | ||
} | ||
} | ||
} | ||
/// Spawns a service that binds to the given address using the given handle. | ||
#[doc(hidden)] |
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.
No longer needed.
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.
Done
src/client.rs
Outdated
use native_tls::TlsConnector; | ||
|
||
/// TLS context | ||
pub struct TlsClientContext { |
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 don't feel super strongly about this, but since it's already in the client::tls
module, we might consider just calling it Context
and people can alias it if they so choose. Thoughts? Maybe the tls
module should be top-level and it should be tls::client::Context
?
cc @shaladdle
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.
Yeah, I prefer client::tls::Context
. I prefer client
/server
to be top level, since those seem like core concepts that you'll always use with tarpc.
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.
Yeah, I actually was considering that as well. I didn't particularly like putting Client
in the name, but thought it would be useful if it was available at the root namespace; ::TlsContext
wouldn't be very descriptive.
I like the idea of a root tls
module.
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.
Spoke a bit with Tim. I think he and I are on the same page now. Can we make the crate structure like this:
root
tls
client
server
The client
and server
mods can conditionally generate tls-specific methods or options, and the tls
module can have types that are used in those methods/options. Does that seem reasonable?
src/client.rs
Outdated
{ | ||
#[cfg(not(feature = "tls"))] | ||
inner: | ||
future::Either< |
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.
Can we add a type alias for part of this so we don't have to rewrite the entire thing in both branches of the cfg
?
src/lib.rs
Outdated
extern crate tokio_tls; | ||
pub extern crate native_tls; | ||
|
||
pub use client::tls::TlsClientContext; |
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.
Related to my other comment, I'm starting to feel more strongly that there should be a mod tls
at the crate root, within which is a client module.
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.
Yes, I like this approach too. I will make those changes.
src/macros.rs
Outdated
use util::FirstSocketAddr; | ||
extern crate env_logger; | ||
|
||
macro_rules! t { |
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.
This is basically unwrap on steroids, right? Is there a mnemonic to remember that?
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.
Ha yes. I'm not sure on the history behind the naming though, other than it is short to type...
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.
Short names are nice, but can we name it something that indicates what it does?
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.
We could just call it unwrap!
I guess.
src/lib.rs
Outdated
cfg_if! { | ||
if #[cfg(feature = "tls")] { | ||
extern crate tokio_tls; | ||
pub extern crate native_tls; |
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.
Does this have to be public?
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.
Well we need to expose the TlsAcceptor
type in the builder for the server options. Another option here though instead of just re-exporting the whole crate would be to just re-export the TlsAcceptor
type itself.
I was also thinking on the client
side it could take an option TlsConnector
if people want to override the default. So we could get away with just re-exporting TlsConnector
and TlsAcceptor
from native_tls
instead of the whole crate; that's probably a better idea.
src/server.rs
Outdated
/// Additional options to configure how the server operates. | ||
#[derive(Clone, Default)] | ||
#[cfg_attr(feature = "tls", derive(Default))] | ||
#[cfg_attr(not(feature = "tls"), derive(Clone, Default))] |
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.
Same comment as above: I'd rather impl the intersection of possible traits for each feature.
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.
Done
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.
One general comment: there are a lot of cfg
s now. It's a little scary from a maintainability perspective, but I'm not sure if there's really a good way around it.
src/client.rs
Outdated
use tokio_proto::BindClient; | ||
#[cfg(feature = "tls")] |
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.
Can we use a cfg_if!
to group all the tls imports together?
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.
+1, reducing the number of pieces sprayed around the code with cfg(feature..) is very desirable. If they could somehow just be confined to a single file/module, that would be even better, though I know that might be hard.
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.
Yup, will do.
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.
Sending these now so they're not stale.
@@ -33,7 +36,12 @@ env_logger = "0.3" | |||
futures-cpupool = "0.1" | |||
clap = "2.0" | |||
|
|||
[target.'cfg(target_os = "macos")'.dev-dependencies] |
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.
Does this stuff work on osx/linux/windows? Is OSX the only one that needs an extra dependency like this?
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.
This line will make this dependency apply only for osx. For linux, it internally uses the openssl crate, but we can make required data for testing purposes without needing to bring in the openssl crate.
For windows, I didn't write the test case since I don't actually have access to a windows machine and it seemed a little complicated based on the tests I read in the native-tls
and tokio-tls
crates.
README.md
Outdated
## Example: Futures + TLS | ||
|
||
By default, tarpc uses a `TcpStream` for communication, however you can also opt to use a | ||
`TlsStream<TcpStream>` when the `tls` feature of `tarpc` is enabled. |
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 a sentence or two about what TLS is would be useful here (similar to the beginning of the readme that says what an RPC is).
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've added some additional context now. Let me know how this looks.
README.md
Outdated
|
||
When using TLS, some additional information is required. You will need to make [`TlsAcceptor`] and | ||
`TlsClientContext` structs; `TlsClientContext` requires a [`TlsConnector`]. The [`TlsAcceptor`] and | ||
[`TlsConnector`] structs are defined in the [native-tls] crate which is exposed by `tarpc`. |
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.
This seems like implicit advice to use these structs as exported by tarpc rather than having the user add the native-tls crate as a dependency. Is that the intention? If so is that approach used by any other libraries? I know we re-export some crates already, but my understanding is that those are there to make the macro standalone.
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.
Yes, that's the direction I was going. I've seen libraries that have public interfaces which require structs from other crates generally do this. You can see in reqwest and hyper. I think the best approach would be to just re-export only the external types we need, not the whole crate. I'll make that change when I do my updates.
README.md
Outdated
[native-tls]: https://github.com/sfackler/rust-native-tls | ||
|
||
Enabling the `tls` feature does not require you to use TLS streams. You can still | ||
use TCP streams as well or a combination of both stream types. |
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.
Please clarify that you mean it's ok/possible to use both types of stream in the same binary, rather than being able to use either TLS or non-TLS stream types for the same server.
src/client.rs
Outdated
use native_tls::TlsConnector; | ||
|
||
/// TLS context | ||
pub struct TlsClientContext { |
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.
Yeah, I prefer client::tls::Context
. I prefer client
/server
to be top level, since those seem like core concepts that you'll always use with tarpc.
src/client.rs
Outdated
impl TlsClientContext { | ||
/// Try to make a new `TlsClientContext`, providing the domain the client will | ||
/// connect to. | ||
pub fn try_new<S: Into<String>>(domain: S) |
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.
+1
src/client.rs
Outdated
@@ -91,9 +119,12 @@ impl<Req, Resp, E> fmt::Debug for Client<Req, Resp, E> | |||
} | |||
|
|||
/// Additional options to configure how the client connects and operates. | |||
#[derive(Clone, Default)] | |||
#[cfg_attr(feature = "tls", derive(Default))] | |||
#[cfg_attr(not(feature = "tls"), derive(Clone, Default))] |
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.
+1
src/client.rs
Outdated
use tokio_proto::BindClient; | ||
#[cfg(feature = "tls")] |
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.
+1, reducing the number of pieces sprayed around the code with cfg(feature..) is very desirable. If they could somehow just be confined to a single file/module, that would be even better, though I know that might be hard.
Thanks for all the comments. I'll work on these tomorrow! |
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.
Ok, got through everything :). Thanks so much for the PR. Overall it looks good. Two requests:
Can you add one test that instantiates both a tls and non-tls server and uses both independently? This is mostly meant as a sanity check that something weird doesn't happen when using both.
I'm also curious about error cases related specifically to TLS. What is supposed to happen when a non-TLS client tries to connect to a TLS server? What about the opposite? Can we have tests for these cases?
src/macros.rs
Outdated
use util::FirstSocketAddr; | ||
extern crate env_logger; | ||
|
||
macro_rules! t { |
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.
Short names are nice, but can we name it something that indicates what it does?
src/client.rs
Outdated
use native_tls::TlsConnector; | ||
|
||
/// TLS context | ||
pub struct TlsClientContext { |
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.
Spoke a bit with Tim. I think he and I are on the same page now. Can we make the crate structure like this:
root
tls
client
server
The client
and server
mods can conditionally generate tls-specific methods or options, and the tls
module can have types that are used in those methods/options. Does that seem reasonable?
let rx = match options.reactor { | ||
Some(Reactor::Handle(handle)) => { | ||
let tcp = TcpStream::connect(&addr, &handle).map(MultiplexConnect::new(handle)); |
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.
Instead of reproducing these three lines twice, can we only build the ConnectFn
using cfg
, and do the rest unconditionally?
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.
Good idea. I've implemented that.
src/client.rs
Outdated
@@ -227,6 +352,7 @@ pub mod future { | |||
ConnectFuture { inner: future::Either::B(rx.map_err(panic as fn(_) -> _).flatten()) } | |||
} | |||
} | |||
|
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.
Is this blank line intentional?
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.
Good catch. I have removed it.
src/lib.rs
Outdated
@@ -59,6 +60,53 @@ | |||
//! } | |||
//! ``` | |||
//! | |||
//! Example usage with TLS: | |||
//! | |||
#![cfg_attr(feature = "tls", doc = " ```rust,no_run")] |
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.
What do these cfg
s do?
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.
These provide some configurability for what runs during rustdoc's test.
So for these two:
#![cfg_attr(feature = "tls", doc = " ```ignore")]
#![cfg_attr(not(feature = "tls"), doc = " ```")]
It's interpreted something like this:
if feature != tls { return //! ``` };
if feature == tls { return //! ```ignore };
ignore
just means don't run this test case at all. no_run
is going to compile the test, but not actually run it. We want this for the tls use case because we need to add extra stuff like in the test cases to add the certs to the cert chain, which we don't need to here in the rustdoc.
For the tcp side actually we can run that now even with tls enabled, so the first set of conditions can be removed.
src/server.rs
Outdated
@@ -50,34 +75,48 @@ pub fn listen<S, Req, Resp, E>(new_service: S, addr: SocketAddr, options: Option | |||
Resp: Serialize + 'static, | |||
E: Serialize + 'static | |||
{ | |||
// similar to the client, since `Options` is not `Send`, we take the `TlsAcceptor` when it is |
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: Capitalize
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.
Done
src/server.rs
Outdated
fn listen_with<S, Req, Resp, E>(new_service: S, | ||
addr: SocketAddr, | ||
handle: Handle, | ||
_acceptor: Acceptor) |
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.
Why _
? Is this unused with tls off or something?
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.
Yes, that's correct. We don't use acceptor
in the tcp workflow.
I can't figure out how to reply to one of @shaladdle's comments, so I'm just writing it here. It would be great if a non-tls client trying to connect to a tls server receives a specific tls-related error, like |
I've made updates to incorporate most of the comments (and also updated to latest master). Here are the outstanding items/discussion points (I think!):
|
I still think it should be named |
Okay thanks! Works for me. I've made that change now too. |
The `cfg_attr` logic caused many false warnings from clippy.
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're nearing the end here. Just (hopefully) some small things.
src/lib.rs
Outdated
extern crate tokio_tls; | ||
extern crate native_tls; | ||
|
||
/// Re-exported TLS-related types |
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.
Why do these need to be re-exported?
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.
Because we have public interfaces now that use all these types, except Pkcs12
, but you need that type to make the TlsAcceptor
itself.
src/client.rs
Outdated
} | ||
|
||
impl Context { | ||
/// Try to construct a new `Context`, providing the domain the client will |
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.
s/providing/provided?
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.
Good point, actually this comment is wrong. It's the domain used for validation, not connection. I will update that.
Great! I've updated the comments for |
src/lib.rs
Outdated
|
||
/// Re-exported TLS-related types | ||
pub mod tls { | ||
pub use native_tls::Error as NativeTlsError; |
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.
what about renaming the module native_tls
and then pub use native_tls::Error
? (and then just add it to the below pub use
statement)
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.
Done. Used extern crate native_tls as native_tls_inner
to avoid the name spacing problem.
src/macros.rs
Outdated
const DOMAIN: &'static str = "foobar.com"; | ||
|
||
use client::tls::Context; | ||
use ::native_tls::{Pkcs12, TlsAcceptor, TlsConnector}; |
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 the ::
prefix is redundant?
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.
Fixed.
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, @compressed, this is an incredibly important addition!
a35a3e3
to
c6bc6a8
Compare
You're welcome! I'm excited for it to land. Thanks for all the comments and working with me on it. I just made one more minor change to add |
+1, this is great, @compressed. Thanks so much for contributing :) |
c6bc6a8
to
7c9cca8
Compare
So here we go... TLS support take two!
The
Options
change and recent refactoring made this change much easier to make. I didn't need to modify the macro at all. I was able to re-use all the tests as well with some refactoring. I added thet!
macro there in testing (it's common in a few other libraries), mainly because it provides better errors (line numbers) on mac. I can change those to unwraps though or change everything tot!
if you want.I think most of the changes are straight-forward. The only tricky part I ran into was that
Options
is notSend
, so I needed to pull out the related TLS part ofOptions
on both the client and server in order to make the compiler happy.Note: I did a merge because the changes in both this codebase and my branch were so much that the rebase was turning out to be quite ugly. I believe you can do a merge and squash and GitHub though.
Fix #77