Merge branch 'master' into master

This commit is contained in:
zero-systems 2020-02-02 18:11:46 +10:00 committed by GitHub
commit 857d81e457
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 393 additions and 189 deletions

View File

@ -27,26 +27,6 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ matrix.version }}-x86_64-pc-windows-msvc-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ matrix.version }}-x86_64-pc-windows-msvc-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ matrix.version }}-x86_64-pc-windows-msvc-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Install OpenSSL - name: Install OpenSSL
run: | run: |
vcpkg integrate install vcpkg integrate install
@ -74,8 +54,5 @@ jobs:
--skip=test_expect_continue --skip=test_expect_continue
--skip=test_http10_keepalive --skip=test_http10_keepalive
--skip=test_slow_request --skip=test_slow_request
--skip=test_connection_force_close
- name: Clear the cargo caches --skip=test_connection_server_close
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,11 +1,16 @@
# Changes # Changes
## [2.0.NEXT] - 2020-01-xx ## [2.0.NEXT] - 2020-01-xx
### Changed ### Changed
* Use `sha-1` crate instead of unmaintained `sha1` crate * Use `sha-1` crate instead of unmaintained `sha1` crate
* Skip empty chunks when returning response from a `Stream` #1308
* Update the `time` dependency to 0.2.5
## [2.0.0] - 2019-12-25 ## [2.0.0] - 2019-12-25
### Changed ### Changed

View File

@ -87,7 +87,7 @@ regex = "1.3"
serde = { version = "1.0", features=["derive"] } serde = { version = "1.0", features=["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.6.1"
time = "0.1.42" time = { version = "0.2.5", default-features = false, features = ["std"] }
url = "2.1" url = "2.1"
open-ssl = { version="0.10", package = "openssl", optional = true } open-ssl = { version="0.10", package = "openssl", optional = true }
rust-tls = { version = "0.16.0", package = "rustls", optional = true } rust-tls = { version = "0.16.0", package = "rustls", optional = true }

View File

@ -2,6 +2,10 @@
# [Unreleased] # [Unreleased]
### Changed
* Update the `time` dependency to 0.2.5
### Fixed ### Fixed
* Allow `SameSite=None` cookies to be sent in a response. * Allow `SameSite=None` cookies to be sent in a response.

View File

@ -52,7 +52,6 @@ base64 = "0.11"
bitflags = "1.2" bitflags = "1.2"
bytes = "0.5.3" bytes = "0.5.3"
copyless = "0.1.4" copyless = "0.1.4"
chrono = "0.4.6"
derive_more = "0.99.2" derive_more = "0.99.2"
either = "1.5.3" either = "1.5.3"
encoding_rs = "0.8" encoding_rs = "0.8"
@ -77,7 +76,7 @@ serde_json = "1.0"
sha-1 = "0.8" sha-1 = "0.8"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.6.1"
time = "0.1.42" time = { version = "0.2.5", default-features = false, features = ["std"] }
# for secure cookie # for secure cookie
ring = { version = "0.16.9", optional = true } ring = { version = "0.16.9", optional = true }

View File

@ -5,6 +5,7 @@ use std::{fmt, mem};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::Stream; use futures_core::Stream;
use futures_util::ready;
use pin_project::{pin_project, project}; use pin_project::{pin_project, project};
use crate::error::Error; use crate::error::Error;
@ -360,10 +361,8 @@ impl MessageBody for String {
/// Type represent streaming body. /// Type represent streaming body.
/// Response does not contain `content-length` header and appropriate transfer encoding is used. /// Response does not contain `content-length` header and appropriate transfer encoding is used.
#[pin_project]
pub struct BodyStream<S, E> { pub struct BodyStream<S, E> {
#[pin] stream: Pin<Box<S>>,
stream: S,
_t: PhantomData<E>, _t: PhantomData<E>,
} }
@ -374,7 +373,7 @@ where
{ {
pub fn new(stream: S) -> Self { pub fn new(stream: S) -> Self {
BodyStream { BodyStream {
stream, stream: Box::pin(stream),
_t: PhantomData, _t: PhantomData,
} }
} }
@ -389,22 +388,27 @@ where
BodySize::Stream BodySize::Stream
} }
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
unsafe { Pin::new_unchecked(self) } let mut stream = self.stream.as_mut();
.project() loop {
.stream return Poll::Ready(match ready!(stream.as_mut().poll_next(cx)) {
.poll_next(cx) Some(Ok(ref bytes)) if bytes.is_empty() => continue,
.map(|res| res.map(|res| res.map_err(std::convert::Into::into))) opt => opt.map(|res| res.map_err(Into::into)),
});
}
} }
} }
/// Type represent streaming body. This body implementation should be used /// Type represent streaming body. This body implementation should be used
/// if total size of stream is known. Data get sent as is without using transfer encoding. /// if total size of stream is known. Data get sent as is without using transfer encoding.
#[pin_project]
pub struct SizedStream<S> { pub struct SizedStream<S> {
size: u64, size: u64,
#[pin] stream: Pin<Box<S>>,
stream: S,
} }
impl<S> SizedStream<S> impl<S> SizedStream<S>
@ -412,7 +416,7 @@ where
S: Stream<Item = Result<Bytes, Error>>, S: Stream<Item = Result<Bytes, Error>>,
{ {
pub fn new(size: u64, stream: S) -> Self { pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream } SizedStream { size, stream: Box::pin(stream) }
} }
} }
@ -424,17 +428,26 @@ where
BodySize::Sized64(self.size) BodySize::Sized64(self.size)
} }
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> { fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<Bytes, Error>>> {
unsafe { Pin::new_unchecked(self) } let mut stream = self.stream.as_mut();
.project() loop {
.stream return Poll::Ready(match ready!(stream.as_mut().poll_next(cx)) {
.poll_next(cx) Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val,
});
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use futures::stream;
use futures_util::future::poll_fn; use futures_util::future::poll_fn;
impl Body { impl Body {
@ -589,4 +602,45 @@ mod tests {
BodySize::Sized(25) BodySize::Sized(25)
); );
} }
mod body_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let mut body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
assert_eq!(
poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("2")),
);
}
}
mod sized_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let mut body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
assert_eq!(
poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("2")),
);
}
}
} }

View File

@ -1,4 +1,4 @@
use std::cell::UnsafeCell; use std::cell::Cell;
use std::fmt::Write; use std::fmt::Write;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
@ -7,7 +7,7 @@ use std::{fmt, net};
use actix_rt::time::{delay_for, delay_until, Delay, Instant}; use actix_rt::time::{delay_for, delay_until, Delay, Instant};
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::{future, FutureExt}; use futures_util::{future, FutureExt};
use time; use time::OffsetDateTime;
// "Sun, 06 Nov 1994 08:49:37 GMT".len() // "Sun, 06 Nov 1994 08:49:37 GMT".len()
const DATE_VALUE_LENGTH: usize = 29; const DATE_VALUE_LENGTH: usize = 29;
@ -211,7 +211,7 @@ impl Date {
} }
fn update(&mut self) { fn update(&mut self) {
self.pos = 0; self.pos = 0;
write!(self, "{}", time::at_utc(time::get_time()).rfc822()).unwrap(); write!(self, "{}", OffsetDateTime::now().format("%a, %d %b %Y %H:%M:%S GMT")).unwrap();
} }
} }
@ -228,24 +228,24 @@ impl fmt::Write for Date {
struct DateService(Rc<DateServiceInner>); struct DateService(Rc<DateServiceInner>);
struct DateServiceInner { struct DateServiceInner {
current: UnsafeCell<Option<(Date, Instant)>>, current: Cell<Option<(Date, Instant)>>,
} }
impl DateServiceInner { impl DateServiceInner {
fn new() -> Self { fn new() -> Self {
DateServiceInner { DateServiceInner {
current: UnsafeCell::new(None), current: Cell::new(None),
} }
} }
fn reset(&self) { fn reset(&self) {
unsafe { (&mut *self.current.get()).take() }; self.current.take();
} }
fn update(&self) { fn update(&self) {
let now = Instant::now(); let now = Instant::now();
let date = Date::new(); let date = Date::new();
*(unsafe { &mut *self.current.get() }) = Some((date, now)); self.current.set(Some((date, now)));
} }
} }
@ -255,7 +255,7 @@ impl DateService {
} }
fn check_date(&self) { fn check_date(&self) {
if unsafe { (&*self.0.current.get()).is_none() } { if self.0.current.get().is_none() {
self.0.update(); self.0.update();
// periodic date update // periodic date update
@ -269,12 +269,12 @@ impl DateService {
fn now(&self) -> Instant { fn now(&self) -> Instant {
self.check_date(); self.check_date();
unsafe { (&*self.0.current.get()).as_ref().unwrap().1 } self.0.current.get().unwrap().1
} }
fn set_date<F: FnMut(&Date)>(&self, mut f: F) { fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
self.check_date(); self.check_date();
f(&unsafe { (&*self.0.current.get()).as_ref().unwrap().0 }) f(&self.0.current.get().unwrap().0);
} }
} }
@ -282,6 +282,19 @@ impl DateService {
mod tests { mod tests {
use super::*; use super::*;
// Test modifying the date from within the closure
// passed to `set_date`
#[test]
fn test_evil_date() {
let service = DateService::new();
// Make sure that `check_date` doesn't try to spawn a task
service.0.update();
service.set_date(|_| {
service.0.reset()
});
}
#[test] #[test]
fn test_date_len() { fn test_date_len() {
assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len()); assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len());

View File

@ -1,7 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use chrono::Duration; use time::{Duration, OffsetDateTime};
use time::Tm;
use super::{Cookie, SameSite}; use super::{Cookie, SameSite};
@ -64,13 +63,13 @@ impl CookieBuilder {
/// use actix_http::cookie::Cookie; /// use actix_http::cookie::Cookie;
/// ///
/// let c = Cookie::build("foo", "bar") /// let c = Cookie::build("foo", "bar")
/// .expires(time::now()) /// .expires(time::OffsetDateTime::now())
/// .finish(); /// .finish();
/// ///
/// assert!(c.expires().is_some()); /// assert!(c.expires().is_some());
/// ``` /// ```
#[inline] #[inline]
pub fn expires(mut self, when: Tm) -> CookieBuilder { pub fn expires(mut self, when: OffsetDateTime) -> CookieBuilder {
self.cookie.set_expires(when); self.cookie.set_expires(when);
self self
} }
@ -108,7 +107,9 @@ impl CookieBuilder {
/// ``` /// ```
#[inline] #[inline]
pub fn max_age_time(mut self, value: Duration) -> CookieBuilder { pub fn max_age_time(mut self, value: Duration) -> CookieBuilder {
self.cookie.set_max_age(value); // Truncate any nanoseconds from the Duration, as they aren't represented within `Max-Age`
// and would cause two otherwise identical `Cookie` instances to not be equivalent to one another.
self.cookie.set_max_age(Duration::seconds(value.whole_seconds()));
self self
} }
@ -212,7 +213,7 @@ impl CookieBuilder {
/// ///
/// ```rust /// ```rust
/// use actix_http::cookie::Cookie; /// use actix_http::cookie::Cookie;
/// use chrono::Duration; /// use time::Duration;
/// ///
/// let c = Cookie::build("foo", "bar") /// let c = Cookie::build("foo", "bar")
/// .permanent() /// .permanent()

View File

@ -1,7 +1,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::mem::replace; use std::mem::replace;
use chrono::Duration; use time::{Duration, OffsetDateTime};
use super::delta::DeltaCookie; use super::delta::DeltaCookie;
use super::Cookie; use super::Cookie;
@ -188,7 +188,7 @@ impl CookieJar {
/// ///
/// ```rust /// ```rust
/// use actix_http::cookie::{CookieJar, Cookie}; /// use actix_http::cookie::{CookieJar, Cookie};
/// use chrono::Duration; /// use time::Duration;
/// ///
/// let mut jar = CookieJar::new(); /// let mut jar = CookieJar::new();
/// ///
@ -202,7 +202,7 @@ impl CookieJar {
/// let delta: Vec<_> = jar.delta().collect(); /// let delta: Vec<_> = jar.delta().collect();
/// assert_eq!(delta.len(), 1); /// assert_eq!(delta.len(), 1);
/// assert_eq!(delta[0].name(), "name"); /// assert_eq!(delta[0].name(), "name");
/// assert_eq!(delta[0].max_age(), Some(Duration::seconds(0))); /// assert_eq!(delta[0].max_age(), Some(Duration::zero()));
/// ``` /// ```
/// ///
/// Removing a new cookie does not result in a _removal_ cookie: /// Removing a new cookie does not result in a _removal_ cookie:
@ -220,8 +220,8 @@ impl CookieJar {
pub fn remove(&mut self, mut cookie: Cookie<'static>) { pub fn remove(&mut self, mut cookie: Cookie<'static>) {
if self.original_cookies.contains(cookie.name()) { if self.original_cookies.contains(cookie.name()) {
cookie.set_value(""); cookie.set_value("");
cookie.set_max_age(Duration::seconds(0)); cookie.set_max_age(Duration::zero());
cookie.set_expires(time::now() - Duration::days(365)); cookie.set_expires(OffsetDateTime::now() - Duration::days(365));
self.delta_cookies.replace(DeltaCookie::removed(cookie)); self.delta_cookies.replace(DeltaCookie::removed(cookie));
} else { } else {
self.delta_cookies.remove(cookie.name()); self.delta_cookies.remove(cookie.name());
@ -239,7 +239,7 @@ impl CookieJar {
/// ///
/// ```rust /// ```rust
/// use actix_http::cookie::{CookieJar, Cookie}; /// use actix_http::cookie::{CookieJar, Cookie};
/// use chrono::Duration; /// use time::Duration;
/// ///
/// let mut jar = CookieJar::new(); /// let mut jar = CookieJar::new();
/// ///
@ -533,7 +533,7 @@ mod test {
#[test] #[test]
#[cfg(feature = "secure-cookies")] #[cfg(feature = "secure-cookies")]
fn delta() { fn delta() {
use chrono::Duration; use time::Duration;
use std::collections::HashMap; use std::collections::HashMap;
let mut c = CookieJar::new(); let mut c = CookieJar::new();
@ -556,7 +556,7 @@ mod test {
assert!(names.get("test2").unwrap().is_none()); assert!(names.get("test2").unwrap().is_none());
assert!(names.get("test3").unwrap().is_none()); assert!(names.get("test3").unwrap().is_none());
assert!(names.get("test4").unwrap().is_none()); assert!(names.get("test4").unwrap().is_none());
assert_eq!(names.get("original").unwrap(), &Some(Duration::seconds(0))); assert_eq!(names.get("original").unwrap(), &Some(Duration::zero()));
} }
#[test] #[test]

View File

@ -65,9 +65,8 @@ use std::borrow::Cow;
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use chrono::Duration;
use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
use time::Tm; use time::{Duration, OffsetDateTime};
pub use self::builder::CookieBuilder; pub use self::builder::CookieBuilder;
pub use self::draft::*; pub use self::draft::*;
@ -172,7 +171,7 @@ pub struct Cookie<'c> {
/// The cookie's value. /// The cookie's value.
value: CookieStr, value: CookieStr,
/// The cookie's expiration, if any. /// The cookie's expiration, if any.
expires: Option<Tm>, expires: Option<OffsetDateTime>,
/// The cookie's maximum age, if any. /// The cookie's maximum age, if any.
max_age: Option<Duration>, max_age: Option<Duration>,
/// The cookie's domain, if any. /// The cookie's domain, if any.
@ -479,7 +478,7 @@ impl<'c> Cookie<'c> {
/// assert_eq!(c.max_age(), None); /// assert_eq!(c.max_age(), None);
/// ///
/// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap();
/// assert_eq!(c.max_age().map(|age| age.num_hours()), Some(1)); /// assert_eq!(c.max_age().map(|age| age.whole_hours()), Some(1));
/// ``` /// ```
#[inline] #[inline]
pub fn max_age(&self) -> Option<Duration> { pub fn max_age(&self) -> Option<Duration> {
@ -544,10 +543,10 @@ impl<'c> Cookie<'c> {
/// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT";
/// let cookie_str = format!("name=value; Expires={}", expire_time); /// let cookie_str = format!("name=value; Expires={}", expire_time);
/// let c = Cookie::parse(cookie_str).unwrap(); /// let c = Cookie::parse(cookie_str).unwrap();
/// assert_eq!(c.expires().map(|t| t.tm_year), Some(117)); /// assert_eq!(c.expires().map(|t| t.year()), Some(2017));
/// ``` /// ```
#[inline] #[inline]
pub fn expires(&self) -> Option<Tm> { pub fn expires(&self) -> Option<OffsetDateTime> {
self.expires self.expires
} }
@ -645,7 +644,7 @@ impl<'c> Cookie<'c> {
/// ///
/// ```rust /// ```rust
/// use actix_http::cookie::Cookie; /// use actix_http::cookie::Cookie;
/// use chrono::Duration; /// use time::Duration;
/// ///
/// let mut c = Cookie::new("name", "value"); /// let mut c = Cookie::new("name", "value");
/// assert_eq!(c.max_age(), None); /// assert_eq!(c.max_age(), None);
@ -698,18 +697,19 @@ impl<'c> Cookie<'c> {
/// ///
/// ```rust /// ```rust
/// use actix_http::cookie::Cookie; /// use actix_http::cookie::Cookie;
/// use time::{Duration, OffsetDateTime};
/// ///
/// let mut c = Cookie::new("name", "value"); /// let mut c = Cookie::new("name", "value");
/// assert_eq!(c.expires(), None); /// assert_eq!(c.expires(), None);
/// ///
/// let mut now = time::now(); /// let mut now = OffsetDateTime::now();
/// now.tm_year += 1; /// now += Duration::week();
/// ///
/// c.set_expires(now); /// c.set_expires(now);
/// assert!(c.expires().is_some()) /// assert!(c.expires().is_some())
/// ``` /// ```
#[inline] #[inline]
pub fn set_expires(&mut self, time: Tm) { pub fn set_expires(&mut self, time: OffsetDateTime) {
self.expires = Some(time); self.expires = Some(time);
} }
@ -720,7 +720,7 @@ impl<'c> Cookie<'c> {
/// ///
/// ```rust /// ```rust
/// use actix_http::cookie::Cookie; /// use actix_http::cookie::Cookie;
/// use chrono::Duration; /// use time::Duration;
/// ///
/// let mut c = Cookie::new("foo", "bar"); /// let mut c = Cookie::new("foo", "bar");
/// assert!(c.expires().is_none()); /// assert!(c.expires().is_none());
@ -733,7 +733,7 @@ impl<'c> Cookie<'c> {
pub fn make_permanent(&mut self) { pub fn make_permanent(&mut self) {
let twenty_years = Duration::days(365 * 20); let twenty_years = Duration::days(365 * 20);
self.set_max_age(twenty_years); self.set_max_age(twenty_years);
self.set_expires(time::now() + twenty_years); self.set_expires(OffsetDateTime::now() + twenty_years);
} }
fn fmt_parameters(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt_parameters(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -758,11 +758,11 @@ impl<'c> Cookie<'c> {
} }
if let Some(max_age) = self.max_age() { if let Some(max_age) = self.max_age() {
write!(f, "; Max-Age={}", max_age.num_seconds())?; write!(f, "; Max-Age={}", max_age.whole_seconds())?;
} }
if let Some(time) = self.expires() { if let Some(time) = self.expires() {
write!(f, "; Expires={}", time.rfc822())?; write!(f, "; Expires={}", time.format("%a, %d %b %Y %H:%M:%S GMT"))?;
} }
Ok(()) Ok(())
@ -990,7 +990,7 @@ impl<'a, 'b> PartialEq<Cookie<'b>> for Cookie<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Cookie, SameSite}; use super::{Cookie, SameSite};
use time::strptime; use time::{offset, PrimitiveDateTime};
#[test] #[test]
fn format() { fn format() {
@ -1015,7 +1015,7 @@ mod tests {
assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org"); assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org");
let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S").unwrap().using_offset(offset!(UTC));
let cookie = Cookie::build("foo", "bar").expires(expires).finish(); let cookie = Cookie::build("foo", "bar").expires(expires).finish();
assert_eq!( assert_eq!(
&cookie.to_string(), &cookie.to_string(),

View File

@ -5,11 +5,13 @@ use std::error::Error;
use std::fmt; use std::fmt;
use std::str::Utf8Error; use std::str::Utf8Error;
use chrono::Duration;
use percent_encoding::percent_decode; use percent_encoding::percent_decode;
use time::{Duration, offset};
use super::{Cookie, CookieStr, SameSite}; use super::{Cookie, CookieStr, SameSite};
use crate::time_parser;
/// Enum corresponding to a parsing error. /// Enum corresponding to a parsing error.
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ParseError { pub enum ParseError {
@ -147,7 +149,7 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
Ok(val) => { Ok(val) => {
// Don't panic if the max age seconds is greater than what's supported by // Don't panic if the max age seconds is greater than what's supported by
// `Duration`. // `Duration`.
let val = cmp::min(val, Duration::max_value().num_seconds()); let val = cmp::min(val, Duration::max_value().whole_seconds());
Some(Duration::seconds(val)) Some(Duration::seconds(val))
} }
Err(_) => continue, Err(_) => continue,
@ -179,16 +181,14 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
} }
} }
("expires", Some(v)) => { ("expires", Some(v)) => {
// Try strptime with three date formats according to // Try parsing with three date formats according to
// http://tools.ietf.org/html/rfc2616#section-3.3.1. Try // http://tools.ietf.org/html/rfc2616#section-3.3.1. Try
// additional ones as encountered in the real world. // additional ones as encountered in the real world.
let tm = time::strptime(v, "%a, %d %b %Y %H:%M:%S %Z") let tm = time_parser::parse_http_date(v)
.or_else(|_| time::strptime(v, "%A, %d-%b-%y %H:%M:%S %Z")) .or_else(|| time::parse(v, "%a, %d-%b-%Y %H:%M:%S").ok());
.or_else(|_| time::strptime(v, "%a, %d-%b-%Y %H:%M:%S %Z"))
.or_else(|_| time::strptime(v, "%a %b %d %H:%M:%S %Y"));
if let Ok(time) = tm { if let Some(time) = tm {
cookie.expires = Some(time) cookie.expires = Some(time.using_offset(offset!(UTC)))
} }
} }
_ => { _ => {
@ -216,8 +216,7 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Cookie, SameSite}; use super::{Cookie, SameSite};
use chrono::Duration; use time::{offset, Duration, PrimitiveDateTime};
use time::strptime;
macro_rules! assert_eq_parse { macro_rules! assert_eq_parse {
($string:expr, $expected:expr) => { ($string:expr, $expected:expr) => {
@ -377,7 +376,7 @@ mod tests {
); );
let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S").unwrap().using_offset(offset!(UTC));
expected.set_expires(expires); expected.set_expires(expires);
assert_eq_parse!( assert_eq_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
@ -386,7 +385,7 @@ mod tests {
); );
unexpected.set_domain("foo.com"); unexpected.set_domain("foo.com");
let bad_expires = strptime(time_str, "%a, %d %b %Y %H:%S:%M %Z").unwrap(); let bad_expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%S:%M").unwrap().using_offset(offset!(UTC));
expected.set_expires(bad_expires); expected.set_expires(bad_expires);
assert_ne_parse!( assert_ne_parse!(
" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
@ -414,8 +413,9 @@ mod tests {
#[test] #[test]
fn do_not_panic_on_large_max_ages() { fn do_not_panic_on_large_max_ages() {
let max_seconds = Duration::max_value().num_seconds(); let max_duration = Duration::max_value();
let expected = Cookie::build("foo", "bar").max_age(max_seconds).finish(); let expected = Cookie::build("foo", "bar").max_age_time(max_duration).finish();
assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected); let overflow_duration = max_duration.checked_add(Duration::nanoseconds(1)).unwrap_or(max_duration);
assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", overflow_duration.whole_seconds()), expected);
} }
} }

View File

@ -60,6 +60,12 @@ impl Error {
} }
} }
/// A struct with a private constructor, for use with
/// `__private_get_type_id__`. Its single field is private,
/// ensuring that it can only be constructed from this module
#[doc(hidden)]
pub struct PrivateHelper(());
/// Error that can be converted to `Response` /// Error that can be converted to `Response`
pub trait ResponseError: fmt::Debug + fmt::Display { pub trait ResponseError: fmt::Debug + fmt::Display {
/// Response's status code /// Response's status code
@ -83,19 +89,37 @@ pub trait ResponseError: fmt::Debug + fmt::Display {
resp.set_body(Body::from(buf)) resp.set_body(Body::from(buf))
} }
/// A helper method to get the type ID of the type
/// this trait is implemented on.
/// This method is unsafe to *implement*, since `downcast_ref` relies
/// on the returned `TypeId` to perform a cast.
///
/// Unfortunately, Rust has no notion of a trait method that is
/// unsafe to implement (marking it as `unsafe` makes it unsafe
/// to *call*). As a workaround, we require this method
/// to return a private type along with the `TypeId`. This
/// private type (`PrivateHelper`) has a private constructor,
/// making it impossible for safe code to construct outside of
/// this module. This ensures that safe code cannot violate
/// type-safety by implementing this method.
#[doc(hidden)] #[doc(hidden)]
fn __private_get_type_id__(&self) -> TypeId fn __private_get_type_id__(&self) -> (TypeId, PrivateHelper)
where where
Self: 'static, Self: 'static,
{ {
TypeId::of::<Self>() (TypeId::of::<Self>(), PrivateHelper(()))
} }
} }
impl dyn ResponseError + 'static { impl dyn ResponseError + 'static {
/// Downcasts a response error to a specific type. /// Downcasts a response error to a specific type.
pub fn downcast_ref<T: ResponseError + 'static>(&self) -> Option<&T> { pub fn downcast_ref<T: ResponseError + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__() == TypeId::of::<T>() { if self.__private_get_type_id__().0 == TypeId::of::<T>() {
// Safety: external crates cannot override the default
// implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore
// rely on the returned `TypeId`, which ensures that this
// case is correct.
unsafe { Some(&*(self as *const dyn ResponseError as *const T)) } unsafe { Some(&*(self as *const dyn ResponseError as *const T)) }
} else { } else {
None None

View File

@ -1,59 +1,46 @@
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::io::Write; use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use bytes::{buf::BufMutExt, BytesMut}; use bytes::{buf::BufMutExt, BytesMut};
use http::header::{HeaderValue, InvalidHeaderValue}; use http::header::{HeaderValue, InvalidHeaderValue};
use time::{PrimitiveDateTime, OffsetDateTime, offset};
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::IntoHeaderValue; use crate::header::IntoHeaderValue;
use crate::time_parser;
/// A timestamp with HTTP formatting and parsing /// A timestamp with HTTP formatting and parsing
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(time::Tm); pub struct HttpDate(OffsetDateTime);
impl FromStr for HttpDate { impl FromStr for HttpDate {
type Err = ParseError; type Err = ParseError;
fn from_str(s: &str) -> Result<HttpDate, ParseError> { fn from_str(s: &str) -> Result<HttpDate, ParseError> {
match time::strptime(s, "%a, %d %b %Y %T %Z") match time_parser::parse_http_date(s) {
.or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z")) Some(t) => Ok(HttpDate(t.using_offset(offset!(UTC)))),
.or_else(|_| time::strptime(s, "%c")) None => Err(ParseError::Header)
{
Ok(t) => Ok(HttpDate(t)),
Err(_) => Err(ParseError::Header),
} }
} }
} }
impl Display for HttpDate { impl Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0.to_utc().rfc822(), f) fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f)
} }
} }
impl From<time::Tm> for HttpDate { impl From<OffsetDateTime> for HttpDate {
fn from(tm: time::Tm) -> HttpDate { fn from(dt: OffsetDateTime) -> HttpDate {
HttpDate(tm) HttpDate(dt)
} }
} }
impl From<SystemTime> for HttpDate { impl From<SystemTime> for HttpDate {
fn from(sys: SystemTime) -> HttpDate { fn from(sys: SystemTime) -> HttpDate {
let tmspec = match sys.duration_since(UNIX_EPOCH) { HttpDate(PrimitiveDateTime::from(sys).using_offset(offset!(UTC)))
Ok(dur) => {
time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32)
}
Err(err) => {
let neg = err.duration();
time::Timespec::new(
-(neg.as_secs() as i64),
-(neg.subsec_nanos() as i32),
)
}
};
HttpDate(time::at_utc(tmspec))
} }
} }
@ -62,56 +49,45 @@ impl IntoHeaderValue for HttpDate {
fn try_into(self) -> Result<HeaderValue, Self::Error> { fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = BytesMut::with_capacity(29).writer(); let mut wrt = BytesMut::with_capacity(29).writer();
write!(wrt, "{}", self.0.rfc822()).unwrap(); write!(wrt, "{}", self.0.to_offset(offset!(UTC)).format("%a, %d %b %Y %H:%M:%S GMT")).unwrap();
HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze()) HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze())
} }
} }
impl From<HttpDate> for SystemTime { impl From<HttpDate> for SystemTime {
fn from(date: HttpDate) -> SystemTime { fn from(date: HttpDate) -> SystemTime {
let spec = date.0.to_timespec(); let dt = date.0;
if spec.sec >= 0 { let epoch = OffsetDateTime::unix_epoch();
UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32)
} else { UNIX_EPOCH + (dt - epoch)
UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32)
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::HttpDate; use super::HttpDate;
use time::Tm; use time::{PrimitiveDateTime, date, time, offset};
const NOV_07: HttpDate = HttpDate(Tm {
tm_nsec: 0,
tm_sec: 37,
tm_min: 48,
tm_hour: 8,
tm_mday: 7,
tm_mon: 10,
tm_year: 94,
tm_wday: 0,
tm_isdst: 0,
tm_yday: 0,
tm_utcoff: 0,
});
#[test] #[test]
fn test_date() { fn test_date() {
let nov_07 = HttpDate(PrimitiveDateTime::new(
date!(1994-11-07),
time!(8:48:37)
).using_offset(offset!(UTC)));
assert_eq!( assert_eq!(
"Sun, 07 Nov 1994 08:48:37 GMT".parse::<HttpDate>().unwrap(), "Sun, 07 Nov 1994 08:48:37 GMT".parse::<HttpDate>().unwrap(),
NOV_07 nov_07
); );
assert_eq!( assert_eq!(
"Sunday, 07-Nov-94 08:48:37 GMT" "Sunday, 07-Nov-94 08:48:37 GMT"
.parse::<HttpDate>() .parse::<HttpDate>()
.unwrap(), .unwrap(),
NOV_07 nov_07
); );
assert_eq!( assert_eq!(
"Sun Nov 7 08:48:37 1994".parse::<HttpDate>().unwrap(), "Sun Nov 7 08:48:37 1994".parse::<HttpDate>().unwrap(),
NOV_07 nov_07
); );
assert!("this-is-no-date".parse::<HttpDate>().is_err()); assert!("this-is-no-date".parse::<HttpDate>().is_err());
} }

View File

@ -27,6 +27,7 @@ mod payload;
mod request; mod request;
mod response; mod response;
mod service; mod service;
mod time_parser;
pub mod cookie; pub mod cookie;
pub mod error; pub mod error;

View File

@ -0,0 +1,42 @@
use time::{PrimitiveDateTime, Date};
/// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime.
pub fn parse_http_date(time: &str) -> Option<PrimitiveDateTime> {
try_parse_rfc_1123(time)
.or_else(|| try_parse_rfc_850(time))
.or_else(|| try_parse_asctime(time))
}
/// Attempt to parse a `time` string as a RFC 1123 formatted date time string.
fn try_parse_rfc_1123(time: &str) -> Option<PrimitiveDateTime> {
time::parse(time, "%a, %d %b %Y %H:%M:%S").ok()
}
/// Attempt to parse a `time` string as a RFC 850 formatted date time string.
fn try_parse_rfc_850(time: &str) -> Option<PrimitiveDateTime> {
match PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S") {
Ok(dt) => {
// If the `time` string contains a two-digit year, then as per RFC 2616 § 19.3,
// we consider the year as part of this century if it's within the next 50 years,
// otherwise we consider as part of the previous century.
let now = PrimitiveDateTime::now();
let century_start_year = (now.year() / 100) * 100;
let mut expanded_year = century_start_year + dt.year();
if expanded_year > now.year() + 50 {
expanded_year -= 100;
}
match Date::try_from_ymd(expanded_year, dt.month(), dt.day()) {
Ok(date) => Some(PrimitiveDateTime::new(date, dt.time())),
Err(_) => None
}
}
Err(_) => None
}
}
/// Attempt to parse a `time` string using ANSI C's `asctime` format.
fn try_parse_asctime(time: &str) -> Option<PrimitiveDateTime> {
time::parse(time, "%a %b %_d %H:%M:%S %Y").ok()
}

View File

@ -1,5 +1,9 @@
# Changes # Changes
## [Unreleased] - 2020-xx-xx
* Update the `time` dependency to 0.2.5
## [0.2.1] - 2020-01-10 ## [0.2.1] - 2020-01-10
* Fix panic with already borrowed: BorrowMutError #1263 * Fix panic with already borrowed: BorrowMutError #1263

View File

@ -21,7 +21,7 @@ actix-service = "1.0.2"
futures = "0.3.1" futures = "0.3.1"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
time = "0.1.42" time = { version = "0.2.5", default-features = false, features = ["std"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "1.0.0" actix-rt = "1.0.0"

View File

@ -428,14 +428,14 @@ impl CookieIdentityInner {
let now = SystemTime::now(); let now = SystemTime::now();
if let Some(visit_deadline) = self.visit_deadline { if let Some(visit_deadline) = self.visit_deadline {
if now.duration_since(value.visit_timestamp?).ok()? if now.duration_since(value.visit_timestamp?).ok()?
> visit_deadline.to_std().ok()? > visit_deadline
{ {
return None; return None;
} }
} }
if let Some(login_deadline) = self.login_deadline { if let Some(login_deadline) = self.login_deadline {
if now.duration_since(value.login_timestamp?).ok()? if now.duration_since(value.login_timestamp?).ok()?
> login_deadline.to_std().ok()? > login_deadline
{ {
return None; return None;
} }
@ -855,7 +855,7 @@ mod tests {
let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap(); let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap();
assert_eq!(cv.identity, identity); assert_eq!(cv.identity, identity);
let now = SystemTime::now(); let now = SystemTime::now();
let t30sec_ago = now - Duration::seconds(30).to_std().unwrap(); let t30sec_ago = now - Duration::seconds(30);
match login_timestamp { match login_timestamp {
LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None), LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None),
LoginTimestampCheck::NewTimestamp => assert!( LoginTimestampCheck::NewTimestamp => assert!(
@ -997,7 +997,7 @@ mod tests {
create_identity_server(|c| c.login_deadline(Duration::days(90))).await; create_identity_server(|c| c.login_deadline(Duration::days(90))).await;
let cookie = login_cookie( let cookie = login_cookie(
COOKIE_LOGIN, COOKIE_LOGIN,
Some(SystemTime::now() - Duration::days(180).to_std().unwrap()), Some(SystemTime::now() - Duration::days(180)),
None, None,
); );
let mut resp = test::call_service( let mut resp = test::call_service(
@ -1023,7 +1023,7 @@ mod tests {
let cookie = login_cookie( let cookie = login_cookie(
COOKIE_LOGIN, COOKIE_LOGIN,
None, None,
Some(SystemTime::now() - Duration::days(180).to_std().unwrap()), Some(SystemTime::now() - Duration::days(180)),
); );
let mut resp = test::call_service( let mut resp = test::call_service(
&mut srv, &mut srv,
@ -1065,7 +1065,7 @@ mod tests {
.login_deadline(Duration::days(90)) .login_deadline(Duration::days(90))
}) })
.await; .await;
let timestamp = SystemTime::now() - Duration::days(1).to_std().unwrap(); let timestamp = SystemTime::now() - Duration::days(1);
let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp)); let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
let mut resp = test::call_service( let mut resp = test::call_service(
&mut srv, &mut srv,

View File

@ -1,5 +1,9 @@
# Changes # Changes
## [0.2.1] - 2020-01-xx
* Remove the unused `time` dependency
## [0.2.0] - 2019-12-20 ## [0.2.0] - 2019-12-20
* Release * Release

View File

@ -25,7 +25,6 @@ httparse = "1.3"
futures = "0.3.1" futures = "0.3.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
time = "0.1"
twoway = "0.2" twoway = "0.2"
[dev-dependencies] [dev-dependencies]

View File

@ -1,5 +1,10 @@
# Changes # Changes
## [Unreleased] - 2020-01-xx
* Update the `time` dependency to 0.2.5
* [#1292](https://github.com/actix/actix-web/pull/1292) Long lasting auto-prolonged session
## [0.3.0] - 2019-12-20 ## [0.3.0] - 2019-12-20
* Release * Release

View File

@ -29,7 +29,7 @@ derive_more = "0.99.2"
futures = "0.3.1" futures = "0.3.1"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
time = "0.1.42" time = { version = "0.2.5", default-features = false, features = ["std"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "1.0.0" actix-rt = "1.0.0"

View File

@ -27,6 +27,7 @@ use actix_web::{Error, HttpMessage, ResponseError};
use derive_more::{Display, From}; use derive_more::{Display, From};
use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; use futures::future::{ok, FutureExt, LocalBoxFuture, Ready};
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;
use time::{Duration, OffsetDateTime};
use crate::{Session, SessionStatus}; use crate::{Session, SessionStatus};
@ -56,7 +57,8 @@ struct CookieSessionInner {
domain: Option<String>, domain: Option<String>,
secure: bool, secure: bool,
http_only: bool, http_only: bool,
max_age: Option<time::Duration>, max_age: Option<Duration>,
expires_in: Option<Duration>,
same_site: Option<SameSite>, same_site: Option<SameSite>,
} }
@ -71,6 +73,7 @@ impl CookieSessionInner {
secure: true, secure: true,
http_only: true, http_only: true,
max_age: None, max_age: None,
expires_in: None,
same_site: None, same_site: None,
} }
} }
@ -96,6 +99,10 @@ impl CookieSessionInner {
cookie.set_domain(domain.clone()); cookie.set_domain(domain.clone());
} }
if let Some(expires_in) = self.expires_in {
cookie.set_expires(OffsetDateTime::now() + expires_in);
}
if let Some(max_age) = self.max_age { if let Some(max_age) = self.max_age {
cookie.set_max_age(max_age); cookie.set_max_age(max_age);
} }
@ -123,8 +130,8 @@ impl CookieSessionInner {
fn remove_cookie<B>(&self, res: &mut ServiceResponse<B>) -> Result<(), Error> { fn remove_cookie<B>(&self, res: &mut ServiceResponse<B>) -> Result<(), Error> {
let mut cookie = Cookie::named(self.name.clone()); let mut cookie = Cookie::named(self.name.clone());
cookie.set_value(""); cookie.set_value("");
cookie.set_max_age(time::Duration::seconds(0)); cookie.set_max_age(Duration::zero());
cookie.set_expires(time::now() - time::Duration::days(365)); cookie.set_expires(OffsetDateTime::now() - Duration::days(365));
let val = HeaderValue::from_str(&cookie.to_string())?; let val = HeaderValue::from_str(&cookie.to_string())?;
res.headers_mut().append(SET_COOKIE, val); res.headers_mut().append(SET_COOKIE, val);
@ -263,7 +270,7 @@ impl CookieSession {
/// Sets the `max-age` field in the session cookie being built. /// Sets the `max-age` field in the session cookie being built.
pub fn max_age(self, seconds: i64) -> CookieSession { pub fn max_age(self, seconds: i64) -> CookieSession {
self.max_age_time(time::Duration::seconds(seconds)) self.max_age_time(Duration::seconds(seconds))
} }
/// Sets the `max-age` field in the session cookie being built. /// Sets the `max-age` field in the session cookie being built.
@ -271,6 +278,17 @@ impl CookieSession {
Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
self self
} }
/// Sets the `expires` field in the session cookie being built.
pub fn expires_in(self, seconds: i64) -> CookieSession {
self.expires_in_time(Duration::seconds(seconds))
}
/// Sets the `expires` field in the session cookie being built.
pub fn expires_in_time(mut self, value: Duration) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().expires_in = Some(value);
self
}
} }
impl<S, B: 'static> Transform<S> for CookieSession impl<S, B: 'static> Transform<S> for CookieSession
@ -323,6 +341,7 @@ where
fn call(&mut self, mut req: ServiceRequest) -> Self::Future { fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
let inner = self.inner.clone(); let inner = self.inner.clone();
let (is_new, state) = self.inner.load(&req); let (is_new, state) = self.inner.load(&req);
let prolong_expiration = self.inner.expires_in.is_some();
Session::set_session(state.into_iter(), &mut req); Session::set_session(state.into_iter(), &mut req);
let fut = self.service.call(req); let fut = self.service.call(req);
@ -334,6 +353,9 @@ where
| (SessionStatus::Renewed, Some(state)) => { | (SessionStatus::Renewed, Some(state)) => {
res.checked_expr(|res| inner.set_cookie(res, state)) res.checked_expr(|res| inner.set_cookie(res, state))
} }
(SessionStatus::Unchanged, Some(state)) if prolong_expiration => {
res.checked_expr(|res| inner.set_cookie(res, state))
}
(SessionStatus::Unchanged, _) => (SessionStatus::Unchanged, _) =>
// set a new session cookie upon first request (new client) // set a new session cookie upon first request (new client)
{ {
@ -477,4 +499,47 @@ mod tests {
let body = test::read_response(&mut app, request).await; let body = test::read_response(&mut app, request).await;
assert_eq!(body, Bytes::from_static(b"counter: 100")); assert_eq!(body, Bytes::from_static(b"counter: 100"));
} }
#[actix_rt::test]
async fn prolong_expiration() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::signed(&[0; 32]).secure(false).expires_in(60))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
}))
.service(
web::resource("/test/")
.to(|| async move { "no-changes-in-session" }),
),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
let expires_1 = response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.expect("Cookie is set")
.expires()
.expect("Expiration is set");
actix_rt::time::delay_for(std::time::Duration::from_secs(1)).await;
let request = test::TestRequest::with_uri("/test/").to_request();
let response = app.call(request).await.unwrap();
let expires_2 = response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.expect("Cookie is set")
.expires()
.expect("Expiration is set");
assert!(expires_2 - expires_1 >= Duration::seconds(1));
}
} }

View File

@ -14,7 +14,7 @@ use bytes::Bytes;
use futures::future::{ok, Ready}; use futures::future::{ok, Ready};
use log::debug; use log::debug;
use regex::Regex; use regex::Regex;
use time; use time::OffsetDateTime;
use crate::dev::{BodySize, MessageBody, ResponseBody}; use crate::dev::{BodySize, MessageBody, ResponseBody};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
@ -163,11 +163,11 @@ where
LoggerResponse { LoggerResponse {
fut: self.service.call(req), fut: self.service.call(req),
format: None, format: None,
time: time::now(), time: OffsetDateTime::now(),
_t: PhantomData, _t: PhantomData,
} }
} else { } else {
let now = time::now(); let now = OffsetDateTime::now();
let mut format = self.inner.format.clone(); let mut format = self.inner.format.clone();
for unit in &mut format.0 { for unit in &mut format.0 {
@ -192,7 +192,7 @@ where
{ {
#[pin] #[pin]
fut: S::Future, fut: S::Future,
time: time::Tm, time: OffsetDateTime,
format: Option<Format>, format: Option<Format>,
_t: PhantomData<(B,)>, _t: PhantomData<(B,)>,
} }
@ -242,7 +242,7 @@ pub struct StreamLog<B> {
body: ResponseBody<B>, body: ResponseBody<B>,
format: Option<Format>, format: Option<Format>,
size: usize, size: usize,
time: time::Tm, time: OffsetDateTime,
} }
impl<B> Drop for StreamLog<B> { impl<B> Drop for StreamLog<B> {
@ -366,20 +366,20 @@ impl FormatText {
&self, &self,
fmt: &mut Formatter<'_>, fmt: &mut Formatter<'_>,
size: usize, size: usize,
entry_time: time::Tm, entry_time: OffsetDateTime,
) -> Result<(), fmt::Error> { ) -> Result<(), fmt::Error> {
match *self { match *self {
FormatText::Str(ref string) => fmt.write_str(string), FormatText::Str(ref string) => fmt.write_str(string),
FormatText::Percent => "%".fmt(fmt), FormatText::Percent => "%".fmt(fmt),
FormatText::ResponseSize => size.fmt(fmt), FormatText::ResponseSize => size.fmt(fmt),
FormatText::Time => { FormatText::Time => {
let rt = time::now() - entry_time; let rt = OffsetDateTime::now() - entry_time;
let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; let rt = rt.as_seconds_f64();
fmt.write_fmt(format_args!("{:.6}", rt)) fmt.write_fmt(format_args!("{:.6}", rt))
} }
FormatText::TimeMillis => { FormatText::TimeMillis => {
let rt = time::now() - entry_time; let rt = OffsetDateTime::now() - entry_time;
let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0; let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0;
fmt.write_fmt(format_args!("{:.6}", rt)) fmt.write_fmt(format_args!("{:.6}", rt))
} }
FormatText::EnvironHeader(ref name) => { FormatText::EnvironHeader(ref name) => {
@ -414,7 +414,7 @@ impl FormatText {
} }
} }
fn render_request(&mut self, now: time::Tm, req: &ServiceRequest) { fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
match *self { match *self {
FormatText::RequestLine => { FormatText::RequestLine => {
*self = if req.query_string().is_empty() { *self = if req.query_string().is_empty() {
@ -436,7 +436,7 @@ impl FormatText {
} }
FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()),
FormatText::RequestTime => { FormatText::RequestTime => {
*self = FormatText::Str(now.rfc3339().to_string()) *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S"))
} }
FormatText::RequestHeader(ref name) => { FormatText::RequestHeader(ref name) => {
let s = if let Some(val) = req.headers().get(name) { let s = if let Some(val) = req.headers().get(name) {
@ -513,7 +513,7 @@ mod tests {
.uri("/test/route/yeah") .uri("/test/route/yeah")
.to_srv_request(); .to_srv_request();
let now = time::now(); let now = OffsetDateTime::now();
for unit in &mut format.0 { for unit in &mut format.0 {
unit.render_request(now, &req); unit.render_request(now, &req);
} }
@ -544,7 +544,7 @@ mod tests {
) )
.to_srv_request(); .to_srv_request();
let now = time::now(); let now = OffsetDateTime::now();
for unit in &mut format.0 { for unit in &mut format.0 {
unit.render_request(now, &req); unit.render_request(now, &req);
} }
@ -554,7 +554,7 @@ mod tests {
unit.render_response(&resp); unit.render_response(&resp);
} }
let entry_time = time::now(); let entry_time = OffsetDateTime::now();
let render = |fmt: &mut Formatter<'_>| { let render = |fmt: &mut Formatter<'_>| {
for unit in &format.0 { for unit in &format.0 {
unit.render(fmt, 1024, entry_time)?; unit.render(fmt, 1024, entry_time)?;
@ -572,7 +572,7 @@ mod tests {
let mut format = Format::new("%t"); let mut format = Format::new("%t");
let req = TestRequest::default().to_srv_request(); let req = TestRequest::default().to_srv_request();
let now = time::now(); let now = OffsetDateTime::now();
for unit in &mut format.0 { for unit in &mut format.0 {
unit.render_request(now, &req); unit.render_request(now, &req);
} }
@ -589,6 +589,6 @@ mod tests {
Ok(()) Ok(())
}; };
let s = format!("{}", FormatDisplay(&render)); let s = format!("{}", FormatDisplay(&render));
assert!(s.contains(&format!("{}", now.rfc3339()))); assert!(s.contains(&format!("{}", now.format("%Y-%m-%dT%H:%M:%S"))));
} }
} }

View File

@ -1,5 +1,10 @@
# Changes # Changes
## [Unreleased] - 2020-xx-xx
* Update the `time` dependency to 0.2.5
## [1.0.0] - 2019-12-13 ## [1.0.0] - 2019-12-13
### Changed ### Changed

View File

@ -51,7 +51,7 @@ serde_json = "1.0"
sha1 = "0.6" sha1 = "0.6"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.6.1" serde_urlencoded = "0.6.1"
time = "0.1" time = { version = "0.2.5", default-features = false, features = ["std"] }
open-ssl = { version="0.10", package="openssl", optional = true } open-ssl = { version="0.10", package="openssl", optional = true }
[dev-dependencies] [dev-dependencies]

26
tests/test_weird_poll.rs Normal file
View File

@ -0,0 +1,26 @@
// Regression test for #/1321
use futures::task::{noop_waker, Context};
use futures::stream::once;
use actix_http::body::{MessageBody, BodyStream};
use bytes::Bytes;
#[test]
fn weird_poll() {
let (sender, receiver) = futures::channel::oneshot::channel();
let mut body_stream = Ok(BodyStream::new(once(async {
let x = Box::new(0);
let y = &x;
receiver.await.unwrap();
let _z = **y;
Ok::<_, ()>(Bytes::new())
})));
let waker = noop_waker();
let mut context = Context::from_waker(&waker);
let _ = body_stream.as_mut().unwrap().poll_next(&mut context);
sender.send(()).unwrap();
let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context);
}