diff --git a/Cargo.toml b/Cargo.toml index afca46b6..8ce0ea69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ authors = ["Sean McArthur "] [dependencies.url] git = "https://github.com/servo/rust-url" +[dependencies.openssl] +git = "https://github.com/sfackler/rust-openssl" + [dependencies.mime] git = "https://github.com/seanmonstar/mime.rs" diff --git a/benches/client_mock_tcp.rs b/benches/client_mock_tcp.rs index e2fdc89c..78af12a2 100644 --- a/benches/client_mock_tcp.rs +++ b/benches/client_mock_tcp.rs @@ -84,7 +84,7 @@ impl hyper::header::Header for Foo { impl net::NetworkStream for MockStream { - fn connect(_host: &str, _port: u16) -> IoResult { + fn connect(_host: &str, _port: u16, _scheme: &str) -> IoResult { Ok(MockStream::new()) } diff --git a/src/client/request.rs b/src/client/request.rs index 05b764ae..231240b9 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -55,7 +55,7 @@ impl Request { }; debug!("port={}", port); - let stream: S = try_io!(NetworkStream::connect(host.as_slice(), port)); + let stream: S = try_io!(NetworkStream::connect(host.as_slice(), port, url.scheme.as_slice())); let stream = BufferedWriter::new(stream.abstract()); let mut headers = Headers::new(); headers.set(Host(host)); diff --git a/src/http.rs b/src/http.rs index 8fa65112..7ea5367b 100644 --- a/src/http.rs +++ b/src/http.rs @@ -59,11 +59,13 @@ impl Reader for HttpReader { // None means we don't know the size of the next chunk None => try!(read_chunk_size(body)) }; + debug!("Chunked read, remaining={}", rem); if rem == 0 { // chunk of size 0 signals the end of the chunked stream // if the 0 digit was missing from the stream, it would // be an InvalidInput error instead. + debug!("end of chunked"); return Err(io::standard_error(io::EndOfFile)); } @@ -71,7 +73,12 @@ impl Reader for HttpReader { let count = try!(body.read(buf.mut_slice_to(to_read))); rem -= count; - *opt_remaining = if rem > 0 { Some(rem) } else { None }; + *opt_remaining = if rem > 0 { + Some(rem) + } else { + try!(eat(body, LINE_ENDING)); + None + }; Ok(count) }, EofReader(ref mut body) => { @@ -81,6 +88,16 @@ impl Reader for HttpReader { } } +fn eat(rdr: &mut R, bytes: &[u8]) -> IoResult<()> { + for &b in bytes.iter() { + match try!(rdr.read_byte()) { + byte if byte == b => (), + _ => return Err(io::standard_error(io::InvalidInput)) + } + } + Ok(()) +} + /// Chunked chunks start with 1*HEXDIGIT, indicating the size of the chunk. fn read_chunk_size(rdr: &mut R) -> IoResult { let mut size = 0u; @@ -112,6 +129,7 @@ fn read_chunk_size(rdr: &mut R) -> IoResult { } } } + debug!("chunk size={}", size); Ok(size) } diff --git a/src/lib.rs b/src/lib.rs index a4d26cba..aba37931 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ extern crate time; extern crate url; +extern crate openssl; #[phase(plugin,link)] extern crate log; #[cfg(test)] extern crate test; extern crate "unsafe-any" as uany; diff --git a/src/net.rs b/src/net.rs index 592941c2..215843d0 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,7 +1,12 @@ //! A collection of traits abstracting over Listeners and Streams. -use std::io::{IoResult, Stream, Listener, Acceptor}; +use std::io::{IoResult, IoError, ConnectionAborted, InvalidInput, OtherIoError, + Stream, Listener, Acceptor}; use std::io::net::ip::{SocketAddr, Port}; use std::io::net::tcp::{TcpStream, TcpListener, TcpAcceptor}; +use std::sync::{Arc, Mutex}; + +use openssl::ssl::{SslStream, SslContext, Sslv23}; +use openssl::ssl::error::{SslError, StreamError, OpenSslErrors, SslSessionClosed}; /// The write-status indicating headers have not been written. pub struct Fresh; @@ -32,7 +37,7 @@ pub trait NetworkListener>: Listener IoResult; } -/// An abstraction to receive `HttpStream`s. +/// An abstraction to receive `NetworkStream`s. pub trait NetworkAcceptor: Acceptor + Clone + Send { /// Closes the Acceptor, so no more incoming connections will be handled. fn close(&mut self) -> IoResult<()>; @@ -44,7 +49,7 @@ pub trait NetworkStream: Stream + Clone + Send { fn peer_name(&mut self) -> IoResult; /// Connect to a remote address. - fn connect(host: &str, port: Port) -> IoResult; + fn connect(host: &str, Port, scheme: &str) -> IoResult; /// Turn this into an appropriately typed trait object. #[inline] @@ -113,9 +118,7 @@ pub struct HttpAcceptor { impl Acceptor for HttpAcceptor { #[inline] fn accept(&mut self) -> IoResult { - Ok(HttpStream { - inner: try!(self.inner.accept()) - }) + Ok(Http(try!(self.inner.accept()))) } } @@ -128,39 +131,94 @@ impl NetworkAcceptor for HttpAcceptor { /// A wrapper around a TcpStream. #[deriving(Clone)] -pub struct HttpStream { - inner: TcpStream +pub enum HttpStream { + /// A stream over the HTTP protocol. + Http(TcpStream), + /// A stream over the HTTP protocol, protected by SSL. + // You may be asking wtf an Arc and Mutex? That's because SslStream + // doesn't implement Clone, and we need Clone to use the stream for + // both the Request and Response. + // FIXME: https://github.com/sfackler/rust-openssl/issues/6 + Https(Arc>>, SocketAddr), } impl Reader for HttpStream { #[inline] fn read(&mut self, buf: &mut [u8]) -> IoResult { - self.inner.read(buf) + match *self { + Http(ref mut inner) => inner.read(buf), + Https(ref mut inner, _) => inner.lock().read(buf) + } } } impl Writer for HttpStream { #[inline] fn write(&mut self, msg: &[u8]) -> IoResult<()> { - self.inner.write(msg) + match *self { + Http(ref mut inner) => inner.write(msg), + Https(ref mut inner, _) => inner.lock().write(msg) + } } #[inline] fn flush(&mut self) -> IoResult<()> { - self.inner.flush() + match *self { + Http(ref mut inner) => inner.flush(), + Https(ref mut inner, _) => inner.lock().flush(), + } } } impl NetworkStream for HttpStream { - #[inline] - fn peer_name(&mut self) -> IoResult { - self.inner.peer_name() + fn connect(host: &str, port: Port, scheme: &str) -> IoResult { + match scheme { + "http" => { + debug!("http scheme"); + Ok(Http(try!(TcpStream::connect(host, port)))) + }, + "https" => { + debug!("https scheme"); + let mut stream = try!(TcpStream::connect(host, port)); + // we can't access the tcp stream once it's wrapped in an + // SslStream, so grab the ip address now, just in case. + let addr = try!(stream.peer_name()); + let context = try!(SslContext::new(Sslv23).map_err(lift_ssl_error)); + let stream = try!(SslStream::new(&context, stream).map_err(lift_ssl_error)); + Ok(Https(Arc::new(Mutex::new(stream)), addr)) + }, + _ => { + Err(IoError { + kind: InvalidInput, + desc: "Invalid scheme for Http", + detail: None + }) + } + } } - #[inline] - fn connect(host: &str, port: Port) -> IoResult { - Ok(HttpStream { - inner: try!(TcpStream::connect(host, port)) - }) + fn peer_name(&mut self) -> IoResult { + match *self { + Http(ref mut inner) => inner.peer_name(), + Https(_, addr) => Ok(addr) + } + } +} + +fn lift_ssl_error(ssl: SslError) -> IoError { + match ssl { + StreamError(err) => err, + SslSessionClosed => IoError { + kind: ConnectionAborted, + desc: "SSL Connection Closed", + detail: None + }, + // Unfortunately throw this away. No way to support this + // detail without a better Error abstraction. + OpenSslErrors(errs) => IoError { + kind: OtherIoError, + desc: "Error in OpenSSL", + detail: Some(format!("{}", errs)) + } } }