diff --git a/CHANGES.md b/CHANGES.md index 3aadc8f1e..57304d083 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,19 @@ # Changes +## not released yet -## [1.0.6] - 2019-xx-xx +### Added + +* Add `middleware::Conditon` that conditionally enables another middleware + + +## [1.0.7] - 2019-08-29 + +### Fixed + +* Request Extensions leak #1062 + + +## [1.0.6] - 2019-08-28 ### Added @@ -8,10 +21,17 @@ * Form immplements Responder, returning a `application/x-www-form-urlencoded` response +* Add `into_inner` to `Data` + +* Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set + the header in test requests. + ### Changed * `Query` payload made `pub`. Allows user to pattern-match the payload. +* Enable `rust-tls` feature for client #1045 + * Update serde_urlencoded to 0.6.1 * Update url to 2.1 diff --git a/Cargo.toml b/Cargo.toml index 7c630cc7f..c2d3b0d2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "1.0.5" +version = "1.0.7" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -66,7 +66,7 @@ fail = ["actix-http/fail"] ssl = ["openssl", "actix-server/ssl", "awc/ssl"] # rustls -rust-tls = ["rustls", "actix-server/rust-tls"] +rust-tls = ["rustls", "actix-server/rust-tls", "awc/rust-tls"] # unix domain sockets support uds = ["actix-server/uds"] diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index c8d1b2ae8..c7cdcf0ab 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,12 @@ # Changes +## [0.2.10] - 2019-09-xx + +### Fixed + +* on_connect result isn't added to request extensions for http2 requests #1009 + + ## [0.2.9] - 2019-08-13 ### Changed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 79d7117b4..290a8fbae 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -49,7 +49,7 @@ secure-cookies = ["ring"] [dependencies] actix-service = "0.4.1" actix-codec = "0.1.2" -actix-connect = "0.2.2" +actix-connect = "0.2.4" actix-utils = "0.4.4" actix-server-config = "0.1.2" actix-threadpool = "0.1.1" diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index bab0f5e1e..cd23b7265 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -199,6 +199,7 @@ where self.client_disconnect, ); H2Service::with_config(cfg, service.into_new_service()) + .on_connect(self.on_connect) } /// Finish service configuration and create `HttpService` instance. diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 5e9c0b53d..c82eb4ac8 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -502,7 +502,7 @@ where let pl = self.codec.message_type(); req.head_mut().peer_addr = self.peer_addr; - // on_connect data + // set on_connect data if let Some(ref on_connect) = self.on_connect { on_connect.set(&mut req.extensions_mut()); } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 2bd7940dd..69c620e62 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -23,6 +23,7 @@ use crate::cloneable::CloneableService; use crate::config::ServiceConfig; use crate::error::{DispatchError, Error, ParseError, PayloadError, ResponseError}; use crate::helpers::DataFactory; +use crate::httpmessage::HttpMessage; use crate::message::ResponseHead; use crate::payload::Payload; use crate::request::Request; @@ -122,6 +123,12 @@ where head.version = parts.version; head.headers = parts.headers.into(); head.peer_addr = self.peer_addr; + + // set on_connect data + if let Some(ref on_connect) = self.on_connect { + on_connect.set(&mut req.extensions_mut()); + } + tokio_current_thread::spawn(ServiceResponse:: { state: ServiceResponseState::ServiceCall( self.service.call(req), diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index a74fbb155..a31e4ac89 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -11,6 +11,7 @@ use futures::stream::{once, Stream}; use regex::Regex; use tokio_timer::sleep; +use actix_http::httpmessage::HttpMessage; use actix_http::{ body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, }; @@ -602,3 +603,18 @@ fn test_h1_service_error() { let bytes = srv.load_body(response).unwrap(); assert_eq!(bytes, Bytes::from_static(b"error")); } + +#[test] +fn test_h1_on_connect() { + let mut srv = TestServer::new(|| { + HttpService::build() + .on_connect(|_| 10usize) + .h1(|req: Request| { + assert!(req.extensions().contains::()); + future::ok::<_, ()>(Response::Ok().finish()) + }) + }); + + let response = srv.block_on(srv.get("/").send()).unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-http/tests/test_ssl_server.rs b/actix-http/tests/test_ssl_server.rs index 0b85f33f4..f0c82870d 100644 --- a/actix-http/tests/test_ssl_server.rs +++ b/actix-http/tests/test_ssl_server.rs @@ -1,9 +1,5 @@ #![cfg(feature = "ssl")] use actix_codec::{AsyncRead, AsyncWrite}; -use actix_http::error::{ErrorBadRequest, PayloadError}; -use actix_http::http::header::{self, HeaderName, HeaderValue}; -use actix_http::http::{Method, StatusCode, Version}; -use actix_http::{body, Error, HttpService, Request, Response}; use actix_http_test::TestServer; use actix_server::ssl::OpensslAcceptor; use actix_server_config::ServerConfig; @@ -15,6 +11,12 @@ use futures::stream::{once, Stream}; use openssl::ssl::{AlpnError, SslAcceptor, SslFiletype, SslMethod}; use std::io::Result; +use actix_http::error::{ErrorBadRequest, PayloadError}; +use actix_http::http::header::{self, HeaderName, HeaderValue}; +use actix_http::http::{Method, StatusCode, Version}; +use actix_http::httpmessage::HttpMessage; +use actix_http::{body, Error, HttpService, Request, Response}; + fn load_body(stream: S) -> impl Future where S: Stream, @@ -453,3 +455,26 @@ fn test_h2_service_error() { let bytes = srv.load_body(response).unwrap(); assert!(bytes.is_empty()); } + +#[test] +fn test_h2_on_connect() { + let openssl = ssl_acceptor().unwrap(); + + let mut srv = TestServer::new(move || { + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)) + .and_then( + HttpService::build() + .on_connect(|_| 10usize) + .h2(|req: Request| { + assert!(req.extensions().contains::()); + ok::<_, ()>(Response::Ok().finish()) + }) + .map_err(|_| ()), + ) + }); + + let response = srv.block_on(srv.sget("/").send()).unwrap(); + assert!(response.status().is_success()); +} diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 5edfc5e38..5442e9dbd 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [0.2.5] - 2019-09-06 + +### Changed + +* Ensure that the `Host` header is set when initiating a WebSocket client connection. + ## [0.2.4] - 2019-08-13 ### Changed diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 72c9a38bc..67be9e9d8 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -233,6 +233,10 @@ impl WebsocketsRequest { return Either::A(err(InvalidUrl::UnknownScheme.into())); } + if !self.head.headers.contains_key(header::HOST) { + self.head.headers.insert(header::HOST, HeaderValue::from_str(uri.host().unwrap()).unwrap()); + } + // set cookies if let Some(ref mut jar) = self.cookies { let mut cookie = String::new(); diff --git a/src/data.rs b/src/data.rs index 3461d24f3..14e293bc2 100644 --- a/src/data.rs +++ b/src/data.rs @@ -77,6 +77,11 @@ impl Data { pub fn get_ref(&self) -> &T { self.0.as_ref() } + + /// Convert to the internal Arc + pub fn into_inner(self) -> Arc { + self.0 + } } impl Deref for Data { diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs new file mode 100644 index 000000000..ddc5fdd42 --- /dev/null +++ b/src/middleware/condition.rs @@ -0,0 +1,143 @@ +//! `Middleware` for conditionally enables another middleware. +use actix_service::{Service, Transform}; +use futures::future::{ok, Either, FutureResult, Map}; +use futures::{Future, Poll}; + +/// `Middleware` for conditionally enables another middleware. +/// The controled middleware must not change the `Service` interfaces. +/// This means you cannot control such middlewares like `Logger` or `Compress`. +/// +/// ## Usage +/// +/// ```rust +/// use actix_web::middleware::{Condition, NormalizePath}; +/// use actix_web::App; +/// +/// fn main() { +/// let enable_normalize = std::env::var("NORMALIZE_PATH") == Ok("true".into()); +/// let app = App::new() +/// .wrap(Condition::new(enable_normalize, NormalizePath)); +/// } +/// ``` +pub struct Condition { + trans: T, + enable: bool, +} + +impl Condition { + pub fn new(enable: bool, trans: T) -> Self { + Self { trans, enable } + } +} + +impl Transform for Condition +where + S: Service, + T: Transform, +{ + type Request = S::Request; + type Response = S::Response; + type Error = S::Error; + type InitError = T::InitError; + type Transform = ConditionMiddleware; + type Future = Either< + Map Self::Transform>, + FutureResult, + >; + + fn new_transform(&self, service: S) -> Self::Future { + if self.enable { + let f = self + .trans + .new_transform(service) + .map(ConditionMiddleware::Enable as fn(T::Transform) -> Self::Transform); + Either::A(f) + } else { + Either::B(ok(ConditionMiddleware::Disable(service))) + } + } +} + +pub enum ConditionMiddleware { + Enable(E), + Disable(D), +} + +impl Service for ConditionMiddleware +where + E: Service, + D: Service, +{ + type Request = E::Request; + type Response = E::Response; + type Error = E::Error; + type Future = Either; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + use ConditionMiddleware::*; + match self { + Enable(service) => service.poll_ready(), + Disable(service) => service.poll_ready(), + } + } + + fn call(&mut self, req: E::Request) -> Self::Future { + use ConditionMiddleware::*; + match self { + Enable(service) => Either::A(service.call(req)), + Disable(service) => Either::B(service.call(req)), + } + } +} + +#[cfg(test)] +mod tests { + use actix_service::IntoService; + + use super::*; + use crate::dev::{ServiceRequest, ServiceResponse}; + use crate::error::Result; + use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; + use crate::middleware::errhandlers::*; + use crate::test::{self, TestRequest}; + use crate::HttpResponse; + + fn render_500(mut res: ServiceResponse) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + Ok(ErrorHandlerResponse::Response(res)) + } + + #[test] + fn test_handler_enabled() { + let srv = |req: ServiceRequest| { + req.into_response(HttpResponse::InternalServerError().finish()) + }; + + let mw = + ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + + let mut mw = + test::block_on(Condition::new(true, mw).new_transform(srv.into_service())) + .unwrap(); + let resp = test::call_service(&mut mw, TestRequest::default().to_srv_request()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } + #[test] + fn test_handler_disabled() { + let srv = |req: ServiceRequest| { + req.into_response(HttpResponse::InternalServerError().finish()) + }; + + let mw = + ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + + let mut mw = + test::block_on(Condition::new(false, mw).new_transform(srv.into_service())) + .unwrap(); + + let resp = test::call_service(&mut mw, TestRequest::default().to_srv_request()); + assert_eq!(resp.headers().get(CONTENT_TYPE), None); + } +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 814993f0c..311d0ee99 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -6,7 +6,9 @@ mod defaultheaders; pub mod errhandlers; mod logger; mod normalize; +mod condition; pub use self::defaultheaders::DefaultHeaders; pub use self::logger::Logger; pub use self::normalize::NormalizePath; +pub use self::condition::Condition; diff --git a/src/request.rs b/src/request.rs index 0fc0647ff..6d9d26e8c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -259,6 +259,7 @@ impl Drop for HttpRequest { if Rc::strong_count(&self.0) == 1 { let v = &mut self.0.pool.0.borrow_mut(); if v.len() < 128 { + self.extensions_mut().clear(); v.push(self.0.clone()); } } @@ -494,4 +495,38 @@ mod tests { let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[test] + fn test_extensions_dropped() { + struct Tracker { + pub dropped: bool, + } + struct Foo { + tracker: Rc>, + } + impl Drop for Foo { + fn drop(&mut self) { + self.tracker.borrow_mut().dropped = true; + } + } + + let tracker = Rc::new(RefCell::new(Tracker { dropped: false })); + { + let tracker2 = Rc::clone(&tracker); + let mut srv = init_service(App::new().data(10u32).service( + web::resource("/").to(move |req: HttpRequest| { + req.extensions_mut().insert(Foo { + tracker: Rc::clone(&tracker2), + }); + HttpResponse::Ok() + }), + )); + + let req = TestRequest::default().to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + } + + assert!(tracker.borrow().dropped); + } } diff --git a/src/test.rs b/src/test.rs index 562fdf436..903679cad 100644 --- a/src/test.rs +++ b/src/test.rs @@ -478,6 +478,16 @@ impl TestRequest { self } + /// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type` + /// header is set to `application/x-www-form-urlencoded`. + pub fn set_form(mut self, data: &T) -> Self { + let bytes = serde_urlencoded::to_string(data) + .expect("Failed to serialize test data as a urlencoded form"); + self.req.set_payload(bytes); + self.req.set(ContentType::form_url_encoded()); + self + } + /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is /// set to `application/json`. pub fn set_json(mut self, data: &T) -> Self { @@ -670,6 +680,31 @@ mod tests { assert_eq!(&result.id, "12345"); } + #[test] + fn test_request_response_form() { + let mut app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Form| { + HttpResponse::Ok().json(person.into_inner()) + }), + ))); + + let payload = Person { + id: "12345".to_string(), + name: "User name".to_string(), + }; + + let req = TestRequest::post() + .uri("/people") + .set_form(&payload) + .to_request(); + + assert_eq!(req.content_type(), "application/x-www-form-urlencoded"); + + let result: Person = read_response_json(&mut app, req); + assert_eq!(&result.id, "12345"); + assert_eq!(&result.name, "User name"); + } + #[test] fn test_request_response_json() { let mut app = init_service(App::new().service(web::resource("/people").route(