1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-24 08:22:59 +01:00
actix-web/awc/src/ws.rs

500 lines
16 KiB
Rust
Raw Normal View History

2019-03-28 02:53:19 +01:00
//! Websockets client
2019-03-30 05:13:39 +01:00
use std::fmt::Write as FmtWrite;
use std::net::SocketAddr;
2019-03-28 02:53:19 +01:00
use std::rc::Rc;
use std::{fmt, str};
2019-03-29 22:27:22 +01:00
use actix_codec::Framed;
2019-03-30 05:13:39 +01:00
use actix_http::cookie::{Cookie, CookieJar};
2019-03-28 02:53:19 +01:00
use actix_http::{ws, Payload, RequestHead};
use futures::future::{err, Either, Future};
2019-08-13 19:48:11 +02:00
use percent_encoding::percent_encode;
2019-03-29 06:33:41 +01:00
use tokio_timer::Timeout;
2019-03-28 02:53:19 +01:00
2019-08-13 19:48:11 +02:00
use actix_http::cookie::USERINFO;
2019-03-30 02:51:07 +01:00
pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message};
2019-03-29 22:27:22 +01:00
use crate::connect::BoxedSocket;
2019-03-29 06:33:41 +01:00
use crate::error::{InvalidUrl, SendRequestError, WsClientError};
2019-03-28 02:53:19 +01:00
use crate::http::header::{
self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION,
};
use crate::http::{
ConnectionType, Error as HttpError, HttpTryFrom, Method, StatusCode, Uri, Version,
};
use crate::response::ClientResponse;
2019-03-29 06:33:41 +01:00
use crate::ClientConfig;
2019-03-28 02:53:19 +01:00
/// `WebSocket` connection
pub struct WebsocketsRequest {
pub(crate) head: RequestHead,
2019-03-28 02:53:19 +01:00
err: Option<HttpError>,
origin: Option<HeaderValue>,
protocols: Option<String>,
addr: Option<SocketAddr>,
2019-03-28 02:53:19 +01:00
max_size: usize,
server_mode: bool,
cookies: Option<CookieJar>,
2019-03-29 06:33:41 +01:00
config: Rc<ClientConfig>,
2019-03-28 02:53:19 +01:00
}
impl WebsocketsRequest {
/// Create new websocket connection
2019-03-29 06:33:41 +01:00
pub(crate) fn new<U>(uri: U, config: Rc<ClientConfig>) -> Self
2019-03-28 02:53:19 +01:00
where
Uri: HttpTryFrom<U>,
{
let mut err = None;
let mut head = RequestHead::default();
head.method = Method::GET;
head.version = Version::HTTP_11;
match Uri::try_from(uri) {
Ok(uri) => head.uri = uri,
Err(e) => err = Some(e.into()),
}
WebsocketsRequest {
head,
err,
2019-03-29 06:33:41 +01:00
config,
addr: None,
2019-03-28 02:53:19 +01:00
origin: None,
protocols: None,
max_size: 65_536,
server_mode: false,
cookies: None,
}
}
/// Set socket address of the server.
///
/// This address is used for connection. If address is not
/// provided url's host name get resolved.
pub fn address(mut self, addr: SocketAddr) -> Self {
self.addr = Some(addr);
self
}
2019-03-28 02:53:19 +01:00
/// Set supported websocket protocols
pub fn protocols<U, V>(mut self, protos: U) -> Self
where
2019-04-04 19:59:34 +02:00
U: IntoIterator<Item = V>,
2019-03-28 02:53:19 +01:00
V: AsRef<str>,
{
let mut protos = protos
.into_iter()
.fold(String::new(), |acc, s| acc + s.as_ref() + ",");
protos.pop();
self.protocols = Some(protos);
self
}
/// Set a cookie
2019-07-17 11:08:30 +02:00
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
2019-03-28 02:53:19 +01:00
if self.cookies.is_none() {
let mut jar = CookieJar::new();
jar.add(cookie.into_owned());
self.cookies = Some(jar)
} else {
self.cookies.as_mut().unwrap().add(cookie.into_owned());
}
self
}
/// Set request Origin
pub fn origin<V>(mut self, origin: V) -> Self
where
HeaderValue: HttpTryFrom<V>,
{
match HeaderValue::try_from(origin) {
Ok(value) => self.origin = Some(value),
Err(e) => self.err = Some(e.into()),
}
self
}
/// Set max frame size
///
/// By default max size is set to 64kb
pub fn max_frame_size(mut self, size: usize) -> Self {
self.max_size = size;
self
}
/// Disable payload masking. By default ws client masks frame payload.
pub fn server_mode(mut self) -> Self {
self.server_mode = true;
self
}
/// Append a header.
///
/// Header gets appended to existing header.
/// To override header use `set_header()` method.
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => {
self.head.headers.append(key, value);
}
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
}
self
}
/// Insert a header, replaces existing header.
pub fn set_header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => {
self.head.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
}
self
}
/// Insert a header only if it is not yet set.
pub fn set_header_if_none<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
match HeaderName::try_from(key) {
Ok(key) => {
if !self.head.headers.contains_key(&key) {
match value.try_into() {
Ok(value) => {
self.head.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
}
}
}
Err(e) => self.err = Some(e.into()),
}
self
}
/// Set HTTP basic authorization header
pub fn basic_auth<U>(self, username: U, password: Option<&str>) -> Self
2019-03-28 02:53:19 +01:00
where
U: fmt::Display,
{
let auth = match password {
Some(password) => format!("{}:{}", username, password),
None => format!("{}:", username),
2019-03-28 02:53:19 +01:00
};
self.header(AUTHORIZATION, format!("Basic {}", base64::encode(&auth)))
}
/// Set HTTP bearer authentication header
pub fn bearer_auth<T>(self, token: T) -> Self
where
T: fmt::Display,
{
self.header(AUTHORIZATION, format!("Bearer {}", token))
}
/// Complete request construction and connect to a websockets server.
2019-03-28 02:53:19 +01:00
pub fn connect(
mut self,
2019-03-30 02:51:07 +01:00
) -> impl Future<Item = (ClientResponse, Framed<BoxedSocket, Codec>), Error = WsClientError>
{
2019-03-28 02:53:19 +01:00
if let Some(e) = self.err.take() {
return Either::A(err(e.into()));
}
// validate uri
let uri = &self.head.uri;
if uri.host().is_none() {
return Either::A(err(InvalidUrl::MissingHost.into()));
} else if uri.scheme_part().is_none() {
return Either::A(err(InvalidUrl::MissingScheme.into()));
} else if let Some(scheme) = uri.scheme_part() {
match scheme.as_str() {
"http" | "ws" | "https" | "wss" => (),
_ => return Either::A(err(InvalidUrl::UnknownScheme.into())),
}
} else {
return Either::A(err(InvalidUrl::UnknownScheme.into()));
}
2019-03-30 05:13:39 +01:00
// set cookies
if let Some(ref mut jar) = self.cookies {
2019-03-30 05:13:39 +01:00
let mut cookie = String::new();
for c in jar.delta() {
2019-08-13 19:48:11 +02:00
let name = percent_encode(c.name().as_bytes(), USERINFO);
let value = percent_encode(c.value().as_bytes(), USERINFO);
2019-03-30 05:13:39 +01:00
let _ = write!(&mut cookie, "; {}={}", name, value);
2019-03-28 02:53:19 +01:00
}
self.head.headers.insert(
2019-03-30 05:13:39 +01:00
header::COOKIE,
HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(),
);
2019-03-28 02:53:19 +01:00
}
// origin
if let Some(origin) = self.origin.take() {
self.head.headers.insert(header::ORIGIN, origin);
2019-03-28 02:53:19 +01:00
}
self.head.set_connection_type(ConnectionType::Upgrade);
self.head
.headers
2019-03-28 02:53:19 +01:00
.insert(header::UPGRADE, HeaderValue::from_static("websocket"));
self.head.headers.insert(
2019-03-28 02:53:19 +01:00
header::SEC_WEBSOCKET_VERSION,
HeaderValue::from_static("13"),
);
if let Some(protocols) = self.protocols.take() {
self.head.headers.insert(
2019-03-28 02:53:19 +01:00
header::SEC_WEBSOCKET_PROTOCOL,
HeaderValue::try_from(protocols.as_str()).unwrap(),
);
}
// Generate a random key for the `Sec-WebSocket-Key` header.
// a base64-encoded (see Section 4 of [RFC4648]) value that,
// when decoded, is 16 bytes in length (RFC 6455)
let sec_key: [u8; 16] = rand::random();
let key = base64::encode(&sec_key);
self.head.headers.insert(
2019-03-28 02:53:19 +01:00
header::SEC_WEBSOCKET_KEY,
HeaderValue::try_from(key.as_str()).unwrap(),
);
let head = self.head;
let max_size = self.max_size;
let server_mode = self.server_mode;
2019-03-28 02:53:19 +01:00
let fut = self
2019-03-29 06:33:41 +01:00
.config
2019-03-28 02:53:19 +01:00
.connector
.borrow_mut()
.open_tunnel(head, self.addr)
2019-03-28 02:53:19 +01:00
.from_err()
.and_then(move |(head, framed)| {
// verify response
if head.status != StatusCode::SWITCHING_PROTOCOLS {
return Err(WsClientError::InvalidResponseStatus(head.status));
}
// Check for "UPGRADE" to websocket header
let has_hdr = if let Some(hdr) = head.headers.get(&header::UPGRADE) {
2019-03-28 02:53:19 +01:00
if let Ok(s) = hdr.to_str() {
s.to_ascii_lowercase().contains("websocket")
} else {
false
}
} else {
false
};
if !has_hdr {
log::trace!("Invalid upgrade header");
return Err(WsClientError::InvalidUpgradeHeader);
}
// Check for "CONNECTION" header
if let Some(conn) = head.headers.get(&header::CONNECTION) {
2019-03-28 02:53:19 +01:00
if let Ok(s) = conn.to_str() {
if !s.to_ascii_lowercase().contains("upgrade") {
log::trace!("Invalid connection header: {}", s);
return Err(WsClientError::InvalidConnectionHeader(
conn.clone(),
));
}
} else {
log::trace!("Invalid connection header: {:?}", conn);
2019-05-12 17:34:51 +02:00
return Err(WsClientError::InvalidConnectionHeader(
conn.clone(),
));
2019-03-28 02:53:19 +01:00
}
} else {
log::trace!("Missing connection header");
return Err(WsClientError::MissingConnectionHeader);
}
if let Some(hdr_key) = head.headers.get(&header::SEC_WEBSOCKET_ACCEPT) {
2019-03-28 02:53:19 +01:00
let encoded = ws::hash_key(key.as_ref());
if hdr_key.as_bytes() != encoded.as_bytes() {
log::trace!(
"Invalid challenge response: expected: {} received: {:?}",
encoded,
key
);
return Err(WsClientError::InvalidChallengeResponse(
encoded,
hdr_key.clone(),
));
}
} else {
log::trace!("Missing SEC-WEBSOCKET-ACCEPT header");
return Err(WsClientError::MissingWebSocketAcceptHeader);
};
// response and ws framed
Ok((
ClientResponse::new(head, Payload::None),
framed.map_codec(|_| {
if server_mode {
ws::Codec::new().max_size(max_size)
} else {
ws::Codec::new().max_size(max_size).client_mode()
}
}),
))
});
2019-03-29 06:33:41 +01:00
// set request timeout
if let Some(timeout) = self.config.timeout {
2019-03-29 06:33:41 +01:00
Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| {
if let Some(e) = e.into_inner() {
e
} else {
SendRequestError::Timeout.into()
}
})))
} else {
Either::B(Either::B(fut))
}
2019-03-28 02:53:19 +01:00
}
}
impl fmt::Debug for WebsocketsRequest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(
f,
"\nWebsocketsRequest {}:{}",
self.head.method, self.head.uri
)?;
writeln!(f, " headers:")?;
for (key, val) in self.head.headers.iter() {
writeln!(f, " {:?}: {:?}", key, val)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Client;
#[test]
fn test_debug() {
let request = Client::new().ws("/").header("x-test", "111");
let repr = format!("{:?}", request);
assert!(repr.contains("WebsocketsRequest"));
assert!(repr.contains("x-test"));
}
#[test]
fn test_header_override() {
let req = Client::build()
.header(header::CONTENT_TYPE, "111")
.finish()
.ws("/")
.set_header(header::CONTENT_TYPE, "222");
assert_eq!(
req.head
.headers
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap(),
"222"
);
}
#[test]
fn basic_auth() {
let req = Client::new()
.ws("/")
.basic_auth("username", Some("password"));
assert_eq!(
req.head
.headers
.get(header::AUTHORIZATION)
.unwrap()
.to_str()
.unwrap(),
"Basic dXNlcm5hbWU6cGFzc3dvcmQ="
);
let req = Client::new().ws("/").basic_auth("username", None);
assert_eq!(
req.head
.headers
.get(header::AUTHORIZATION)
.unwrap()
.to_str()
.unwrap(),
"Basic dXNlcm5hbWU6"
);
}
#[test]
fn bearer_auth() {
let req = Client::new().ws("/").bearer_auth("someS3cr3tAutht0k3n");
assert_eq!(
req.head
.headers
.get(header::AUTHORIZATION)
.unwrap()
.to_str()
.unwrap(),
"Bearer someS3cr3tAutht0k3n"
);
2019-04-19 06:28:23 +02:00
let _ = req.connect();
}
#[test]
fn basics() {
2019-05-12 17:34:51 +02:00
let req = Client::new()
.ws("http://localhost/")
.origin("test-origin")
.max_frame_size(100)
.server_mode()
.protocols(&["v1", "v2"])
.set_header_if_none(header::CONTENT_TYPE, "json")
.set_header_if_none(header::CONTENT_TYPE, "text")
.cookie(Cookie::build("cookie1", "value1").finish());
assert_eq!(
req.origin.as_ref().unwrap().to_str().unwrap(),
"test-origin"
);
assert_eq!(req.max_size, 100);
assert_eq!(req.server_mode, true);
assert_eq!(req.protocols, Some("v1,v2".to_string()));
assert_eq!(
req.head.headers.get(header::CONTENT_TYPE).unwrap(),
header::HeaderValue::from_static("json")
);
let _ = actix_http_test::block_fn(move || req.connect());
2019-04-19 06:28:23 +02:00
assert!(Client::new().ws("/").connect().poll().is_err());
assert!(Client::new().ws("http:///test").connect().poll().is_err());
assert!(Client::new()
.ws("hmm://test.com/")
.connect()
.poll()
.is_err());
}
}