-
Notifications
You must be signed in to change notification settings - Fork 25
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
Expose zero-copy-ish headers on ClientRequest #35
Merged
Merged
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
03bf0b5
Expose zero-copy-ish headers on ClientRequest
maciejhirsz dc21081
Minor cleanup
maciejhirsz 26a6131
Cleaner way to check for CRLF header tail
maciejhirsz 6c5ad6a
Keep exposed headers as &[u8]
maciejhirsz 37ba44f
Make that Sec-WebSocket-Key `ArrayVec` is `WebSocketKey`
maciejhirsz 8b43ea7
More pedantic `Sec-WebSocket-Key` handling
maciejhirsz 263bc37
Comment on backtracking 4 bytes
maciejhirsz 5f25ec3
Limit headers to 8kb
maciejhirsz 3f154f8
Clarify ClientRequest::key() comment
maciejhirsz e8487d4
Apply suggestions from code review
maciejhirsz ab4a9d5
Use `try_from` instead of manually checking length
maciejhirsz 46e504b
Document ClientRequest::headers
maciejhirsz b198c3a
Unnecessary lifetime
maciejhirsz 3e110d8
Pedantic camel case
maciejhirsz 3abedb4
Fix doc comment
maciejhirsz 0fa5fd5
Fix cargo doc warning
maciejhirsz 89514e7
.find().is_some() -> .any()
maciejhirsz 45c3383
Fix one clippy nit
maciejhirsz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,8 +10,9 @@ | |
//! | ||
//! [handshake]: https://tools.ietf.org/html/rfc6455#section-4 | ||
|
||
use bytes::{Buf, BytesMut}; | ||
use crate::{Parsing, extension::Extension}; | ||
use arrayvec::ArrayVec; | ||
use bytes::BytesMut; | ||
use crate::extension::Extension; | ||
use crate::connection::{self, Mode}; | ||
use futures::prelude::*; | ||
use sha1::{Digest, Sha1}; | ||
|
@@ -43,6 +44,9 @@ pub struct Server<'a, T> { | |
buffer: BytesMut | ||
} | ||
|
||
/// Owned value of the `Sec-WebSocket-Key` header. | ||
pub type WebSocketKey = ArrayVec<u8, 28>; | ||
|
||
impl<'a, T: AsyncRead + AsyncWrite + Unpin> Server<'a, T> { | ||
/// Create a new server handshake. | ||
pub fn new(socket: T) -> Self { | ||
|
@@ -83,15 +87,25 @@ impl<'a, T: AsyncRead + AsyncWrite + Unpin> Server<'a, T> { | |
} | ||
|
||
/// Await an incoming client handshake request. | ||
pub async fn receive_request(&mut self) -> Result<ClientRequest<'a>, Error> { | ||
pub async fn receive_request<'r>(&'r mut self) -> Result<ClientRequest<'r>, Error> { | ||
self.buffer.clear(); | ||
|
||
let mut skip = 0; | ||
|
||
loop { | ||
crate::read(&mut self.socket, &mut self.buffer, BLOCK_SIZE).await?; | ||
if let Parsing::Done { value, offset } = self.decode_request()? { | ||
self.buffer.advance(offset); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mutable borrow of |
||
return Ok(value) | ||
|
||
// We don't expect body, so can search for the CRLF headers tail from | ||
// the end of the buffer. | ||
if self.buffer[skip..].windows(4).rev().find(|w| w == b"\r\n\r\n").is_some() { | ||
maciejhirsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
break; | ||
} | ||
|
||
// Skip bytes that did not contain CRLF in the next iteration | ||
skip = self.buffer.len().saturating_sub(4); | ||
maciejhirsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
self.decode_request() | ||
} | ||
|
||
/// Respond to the client. | ||
|
@@ -118,32 +132,44 @@ impl<'a, T: AsyncRead + AsyncWrite + Unpin> Server<'a, T> { | |
} | ||
|
||
// Decode client handshake request. | ||
fn decode_request(&mut self) -> Result<Parsing<ClientRequest<'a>>, Error> { | ||
fn decode_request(&mut self) -> Result<ClientRequest, Error> { | ||
let mut header_buf = [httparse::EMPTY_HEADER; MAX_NUM_HEADERS]; | ||
let mut request = httparse::Request::new(&mut header_buf); | ||
|
||
let offset = match request.parse(self.buffer.as_ref()) { | ||
Ok(httparse::Status::Complete(off)) => off, | ||
Ok(httparse::Status::Partial) => return Ok(Parsing::NeedMore(())), | ||
match request.parse(self.buffer.as_ref()) { | ||
Ok(httparse::Status::Complete(_)) => (), | ||
Ok(httparse::Status::Partial) => return Err(Error::IncompleteHttpRequest), | ||
Err(e) => return Err(Error::Http(Box::new(e))) | ||
}; | ||
|
||
if request.method != Some("GET") { | ||
return Err(Error::InvalidRequestMethod) | ||
} | ||
if request.version != Some(1) { | ||
return Err(Error::UnsupportedHttpVersion) | ||
} | ||
|
||
// TODO: Host Validation | ||
with_first_header(&request.headers, "Host", |_h| Ok(()))?; | ||
let host = with_first_header(&request.headers, "Host", |h| Ok(header_to_str(h)))?; | ||
|
||
expect_ascii_header(request.headers, "Upgrade", "websocket")?; | ||
expect_ascii_header(request.headers, "Connection", "upgrade")?; | ||
expect_ascii_header(request.headers, "Sec-WebSocket-Version", "13")?; | ||
|
||
let origin = request.headers.iter().find_map(|h| { | ||
if h.name.eq_ignore_ascii_case("Origin") { | ||
Some(header_to_str(h.value)) | ||
} else { | ||
None | ||
} | ||
}); | ||
let headers = RequestHeaders { host, origin }; | ||
|
||
let ws_key = with_first_header(&request.headers, "Sec-WebSocket-Key", |k| { | ||
Ok(Vec::from(k)) | ||
let mut key = ArrayVec::new(); | ||
|
||
match key.try_extend_from_slice(k) { | ||
Ok(()) => Ok(key), | ||
Err(_) => Err(Error::SecWebsocketKeyTooLong) | ||
} | ||
})?; | ||
|
||
for h in request.headers.iter() | ||
|
@@ -161,14 +187,9 @@ impl<'a, T: AsyncRead + AsyncWrite + Unpin> Server<'a, T> { | |
} | ||
} | ||
|
||
let mut path = String::new(); | ||
if let Some(val) = request.path { | ||
path.push_str(val) | ||
} | ||
let path = request.path.unwrap_or("/"); | ||
dvdplm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Ok(Parsing::Done { | ||
value: ClientRequest { ws_key, protocols, path }, offset, | ||
}) | ||
Ok(ClientRequest { ws_key, protocols, path, headers }) | ||
} | ||
|
||
// Encode server handshake response. | ||
|
@@ -214,12 +235,26 @@ impl<'a, T: AsyncRead + AsyncWrite + Unpin> Server<'a, T> { | |
} | ||
} | ||
|
||
fn header_to_str(bytes: &[u8]) -> &str { | ||
maciejhirsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
str::from_utf8(bytes).unwrap_or("INVALID_UTF8") | ||
maciejhirsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Handshake request received from the client. | ||
#[derive(Debug)] | ||
pub struct ClientRequest<'a> { | ||
ws_key: Vec<u8>, | ||
ws_key: WebSocketKey, | ||
protocols: Vec<&'a str>, | ||
path: String, | ||
path: &'a str, | ||
headers: RequestHeaders<'a>, | ||
} | ||
|
||
/// Select HTTP headers sent by the client. | ||
dvdplm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#[derive(Debug)] | ||
maciejhirsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub struct RequestHeaders<'a> { | ||
/// The [`Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header. | ||
pub host: &'a str, | ||
/// The [`Origin`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin) header, if provided. | ||
pub origin: Option<&'a str>, | ||
} | ||
|
||
impl<'a> ClientRequest<'a> { | ||
|
@@ -228,7 +263,7 @@ impl<'a> ClientRequest<'a> { | |
&self.ws_key | ||
} | ||
|
||
pub fn into_key(self) -> Vec<u8> { | ||
pub fn into_key(self) -> WebSocketKey { | ||
self.ws_key | ||
} | ||
|
||
|
@@ -239,7 +274,11 @@ impl<'a> ClientRequest<'a> { | |
|
||
/// The path the client is requesting. | ||
pub fn path(&self) -> &str { | ||
&self.path | ||
self.path | ||
} | ||
|
||
pub fn headers(&self) -> &RequestHeaders { | ||
&self.headers | ||
maciejhirsz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
|
@@ -321,3 +360,12 @@ const STATUSCODES: &[(u16, &str, &str)] = &[ | |
(511, "511", "Network Authentication Required") | ||
]; | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::WebSocketKey; | ||
|
||
#[test] | ||
fn ws_key_stack_size() { | ||
assert_eq!(32, std::mem::size_of::<WebSocketKey>()); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Attempting decode after each read also made lifetimes convoluted since there is a mutable borrow to
self.buffer
made on every iteration. Opted to do a single parse after we are sure headers are complete in the new code.