Merge branch 'master' into scope-data

This commit is contained in:
Ali MJ Al-Nasrawy 2021-06-16 22:54:57 +03:00 committed by GitHub
commit abc58e2a12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 220 additions and 25 deletions

View File

@ -5,11 +5,15 @@
* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] * `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
### Changed ### Changed
* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162]
[#2162]: (https://github.com/actix/actix-web/pull/2162)
* `ServiceResponse::error_response` now uses body type of `Body`. [#2201] * `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
* `ServiceResponse::checked_expr` now returns a `Result`. [#2201] * `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
* Update `language-tags` to `0.3`. * Update `language-tags` to `0.3`.
* `ServiceResponse::take_body`. [#2201] * `ServiceResponse::take_body`. [#2201]
* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201] * `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] * `middleware::normalize` now will not try to normalize URIs with no valid path [#2246]
### Removed ### Removed

View File

@ -4,10 +4,12 @@
* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] * `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] * For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] * `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
[#2135]: https://github.com/actix/actix-web/pull/2135 [#2135]: https://github.com/actix/actix-web/pull/2135
[#2156]: https://github.com/actix/actix-web/pull/2156 [#2156]: https://github.com/actix/actix-web/pull/2156
[#2225]: https://github.com/actix/actix-web/pull/2225 [#2225]: https://github.com/actix/actix-web/pull/2225
[#2257]: https://github.com/actix/actix-web/pull/2257
## 0.6.0-beta.4 - 2021-04-02 ## 0.6.0-beta.4 - 2021-04-02

View File

@ -279,6 +279,22 @@ mod tests {
); );
} }
#[actix_rt::test]
async fn test_named_file_javascript() {
let file = NamedFile::open("tests/test.js").unwrap();
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"test.js\""
);
}
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_image_attachment() { async fn test_named_file_image_attachment() {
let cd = ContentDisposition { let cd = ContentDisposition {

View File

@ -120,6 +120,11 @@ impl NamedFile {
let disposition = match ct.type_() { let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
mime::APPLICATION => match ct.subtype() {
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
name if name == "wasm" => DispositionType::Inline,
_ => DispositionType::Attachment,
},
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
@ -213,9 +218,11 @@ impl NamedFile {
/// Set the Content-Disposition for serving this file. This allows /// Set the Content-Disposition for serving this file. This allows
/// changing the inline/attachment disposition as well as the filename /// changing the inline/attachment disposition as well as the filename
/// sent to the peer. By default the disposition is `inline` for text, /// sent to the peer.
/// image, and video content types, and `attachment` otherwise, and ///
/// the filename is taken from the path provided in the `open` method /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
/// and the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using. /// after converting it to UTF-8 using.
/// [`std::ffi::OsStr::to_string_lossy`] /// [`std::ffi::OsStr::to_string_lossy`]
#[inline] #[inline]

View File

@ -0,0 +1 @@
// this file is empty.

View File

@ -19,6 +19,7 @@
* Update `language-tags` to `0.3`. * Update `language-tags` to `0.3`.
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] * Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
* `ResponseBuilder::message_body` now returns a `Result`. [#2201] * `ResponseBuilder::message_body` now returns a `Result`. [#2201]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
### Removed ### Removed
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] * Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]

View File

@ -91,6 +91,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" } tls-rustls = { version = "0.19", package = "rustls" }
webpki = { version = "0.21.0" }
[[example]] [[example]]
name = "ws" name = "ws"

View File

@ -171,7 +171,8 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError, InitError = S::InitError,
> { > {
let protos = vec!["h2".to_string().into()]; let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.set_protocols(&protos);
Acceptor::new(config) Acceptor::new(config)

View File

@ -305,7 +305,8 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = (), InitError = (),
> { > {
let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.set_protocols(&protos);
Acceptor::new(config) Acceptor::new(config)

View File

@ -20,10 +20,15 @@ use futures_core::Stream;
use futures_util::stream::{once, StreamExt as _}; use futures_util::stream::{once, StreamExt as _};
use rustls::{ use rustls::{
internal::pemfile::{certs, pkcs8_private_keys}, internal::pemfile::{certs, pkcs8_private_keys},
NoClientAuth, ServerConfig as RustlsServerConfig, NoClientAuth, ServerConfig as RustlsServerConfig, Session,
}; };
use webpki::DNSNameRef;
use std::io::{self, BufReader}; use std::{
io::{self, BufReader, Write},
net::{SocketAddr, TcpStream as StdTcpStream},
sync::Arc,
};
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError> async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
where where
@ -52,6 +57,25 @@ fn tls_config() -> RustlsServerConfig {
config config
} }
pub fn get_negotiated_alpn_protocol(
addr: SocketAddr,
client_alpn_protocol: &[u8],
) -> Option<Vec<u8>> {
let mut config = rustls::ClientConfig::new();
config.alpn_protocols.push(client_alpn_protocol.to_vec());
let mut sess = rustls::ClientSession::new(
&Arc::new(config),
DNSNameRef::try_from_ascii_str("localhost").unwrap(),
);
let mut sock = StdTcpStream::connect(addr).unwrap();
let mut stream = rustls::Stream::new(&mut sess, &mut sock);
// The handshake will fails because the client will not be able to verify the server
// certificate, but it doesn't matter here as we are just interested in the negotiated ALPN
// protocol
let _ = stream.flush();
sess.get_alpn_protocol().map(|proto| proto.to_vec())
}
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() -> io::Result<()> { async fn test_h1() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
@ -460,3 +484,85 @@ async fn test_h1_service_error() {
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(b"error")); assert_eq!(bytes, Bytes::from_static(b"error"));
} }
const H2_ALPN_PROTOCOL: &[u8] = b"h2";
const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1";
const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
#[actix_rt::test]
async fn test_alpn_h1() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}
#[actix_rt::test]
async fn test_alpn_h2() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
Some(H2_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}
#[actix_rt::test]
async fn test_alpn_h2_1() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.finish(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
Some(H2_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), HTTP1_1_ALPN_PROTOCOL),
Some(HTTP1_1_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}

View File

@ -93,9 +93,17 @@ impl ResponseError for UrlencodedError {
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum JsonPayloadError { pub enum JsonPayloadError {
/// Payload size is bigger than allowed. (default: 32kB) /// Payload size is bigger than allowed & content length header set. (default: 2MB)
#[display(fmt = "Json payload size is bigger than allowed")] #[display(
Overflow, fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).",
length,
limit
)]
OverflowKnownLength { length: usize, limit: usize },
/// Payload size is bigger than allowed but no content length header set. (default: 2MB)
#[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)]
Overflow { limit: usize },
/// Content type error /// Content type error
#[display(fmt = "Content type error")] #[display(fmt = "Content type error")]
@ -123,7 +131,11 @@ impl From<PayloadError> for JsonPayloadError {
impl ResponseError for JsonPayloadError { impl ResponseError for JsonPayloadError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
Self::Overflow => StatusCode::PAYLOAD_TOO_LARGE, Self::OverflowKnownLength {
length: _,
limit: _,
} => StatusCode::PAYLOAD_TOO_LARGE,
Self::Overflow { limit: _ } => StatusCode::PAYLOAD_TOO_LARGE,
Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Payload(err) => err.status_code(), Self::Payload(err) => err.status_code(),
_ => StatusCode::BAD_REQUEST, _ => StatusCode::BAD_REQUEST,
@ -208,7 +220,13 @@ mod tests {
#[test] #[test]
fn test_json_payload_error() { fn test_json_payload_error() {
let resp = JsonPayloadError::Overflow.error_response(); let resp = JsonPayloadError::OverflowKnownLength {
length: 0,
limit: 0,
}
.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = JsonPayloadError::Overflow { limit: 0 }.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = JsonPayloadError::ContentType.error_response(); let resp = JsonPayloadError::ContentType.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

View File

@ -368,7 +368,7 @@ where
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
/// Use listener for accepting incoming tls connection requests /// Use listener for accepting incoming tls connection requests
/// ///
/// This method sets alpn protocols to "h2" and "http/1.1" /// This method prepends alpn protocols "h2" and "http/1.1" to configured ones
pub fn listen_rustls( pub fn listen_rustls(
self, self,
lst: net::TcpListener, lst: net::TcpListener,
@ -482,7 +482,7 @@ where
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
/// Start listening for incoming tls connections. /// Start listening for incoming tls connections.
/// ///
/// This method sets alpn protocols to "h2" and "http/1.1" /// This method prepends alpn protocols "h2" and "http/1.1" to configured ones
pub fn bind_rustls<A: net::ToSocketAddrs>( pub fn bind_rustls<A: net::ToSocketAddrs>(
mut self, mut self,
addr: A, addr: A,

View File

@ -240,7 +240,7 @@ pub struct JsonConfig {
} }
impl JsonConfig { impl JsonConfig {
/// Set maximum accepted payload size. By default this limit is 32kB. /// Set maximum accepted payload size. By default this limit is 2MB.
pub fn limit(mut self, limit: usize) -> Self { pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit; self.limit = limit;
self self
@ -273,9 +273,11 @@ impl JsonConfig {
} }
} }
const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb
/// Allow shared refs used as default. /// Allow shared refs used as default.
const DEFAULT_CONFIG: JsonConfig = JsonConfig { const DEFAULT_CONFIG: JsonConfig = JsonConfig {
limit: 32_768, // 2^15 bytes, (~32kB) limit: DEFAULT_LIMIT,
err_handler: None, err_handler: None,
content_type: None, content_type: None,
}; };
@ -349,7 +351,7 @@ where
let payload = payload.take(); let payload = payload.take();
JsonBody::Body { JsonBody::Body {
limit: 32_768, limit: DEFAULT_LIMIT,
length, length,
payload, payload,
buf: BytesMut::with_capacity(8192), buf: BytesMut::with_capacity(8192),
@ -357,7 +359,7 @@ where
} }
} }
/// Set maximum accepted payload size. The default limit is 32kB. /// Set maximum accepted payload size. The default limit is 2MB.
pub fn limit(self, limit: usize) -> Self { pub fn limit(self, limit: usize) -> Self {
match self { match self {
JsonBody::Body { JsonBody::Body {
@ -368,7 +370,10 @@ where
} => { } => {
if let Some(len) = length { if let Some(len) = length {
if len > limit { if len > limit {
return JsonBody::Error(Some(JsonPayloadError::Overflow)); return JsonBody::Error(Some(JsonPayloadError::OverflowKnownLength {
length: len,
limit,
}));
} }
} }
@ -405,8 +410,11 @@ where
match res { match res {
Some(chunk) => { Some(chunk) => {
let chunk = chunk?; let chunk = chunk?;
if (buf.len() + chunk.len()) > *limit { let buf_len = buf.len() + chunk.len();
return Poll::Ready(Err(JsonPayloadError::Overflow)); if buf_len > *limit {
return Poll::Ready(Err(JsonPayloadError::Overflow {
limit: *limit,
}));
} else { } else {
buf.extend_from_slice(&chunk); buf.extend_from_slice(&chunk);
} }
@ -445,7 +453,12 @@ mod tests {
fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool {
match err { match err {
JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow), JsonPayloadError::Overflow { .. } => {
matches!(other, JsonPayloadError::Overflow { .. })
}
JsonPayloadError::OverflowKnownLength { .. } => {
matches!(other, JsonPayloadError::OverflowKnownLength { .. })
}
JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType),
_ => false, _ => false,
} }
@ -538,7 +551,7 @@ mod tests {
let s = Json::<MyObject>::from_request(&req, &mut pl).await; let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(format!("{}", s.err().unwrap()) assert!(format!("{}", s.err().unwrap())
.contains("Json payload size is bigger than allowed")); .contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes)."));
let (req, mut pl) = TestRequest::default() let (req, mut pl) = TestRequest::default()
.insert_header(( .insert_header((
@ -589,7 +602,30 @@ mod tests {
let json = JsonBody::<MyObject>::new(&req, &mut pl, None) let json = JsonBody::<MyObject>::new(&req, &mut pl, None)
.limit(100) .limit(100)
.await; .await;
assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); assert!(json_eq(
json.err().unwrap(),
JsonPayloadError::OverflowKnownLength {
length: 10000,
limit: 100
}
));
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.set_payload(Bytes::from_static(&[0u8; 1000]))
.to_http_parts();
let json = JsonBody::<MyObject>::new(&req, &mut pl, None)
.limit(100)
.await;
assert!(json_eq(
json.err().unwrap(),
JsonPayloadError::Overflow { limit: 100 }
));
let (req, mut pl) = TestRequest::default() let (req, mut pl) = TestRequest::default()
.insert_header(( .insert_header((
@ -686,6 +722,7 @@ mod tests {
assert!(s.is_err()); assert!(s.is_err());
let err_str = s.err().unwrap().to_string(); let err_str = s.err().unwrap().to_string();
assert!(err_str.contains("Json payload size is bigger than allowed")); assert!(err_str
.contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes)."));
} }
} }