diff --git a/ylong_http/src/body/mime/common/headers.rs b/ylong_http/src/body/mime/common/headers.rs index db37a026b1a817305a6540a5649b77494bc08ed7..d89df238d16b02ffa86a87708bd7155c543bc36d 100644 --- a/ylong_http/src/body/mime/common/headers.rs +++ b/ylong_http/src/body/mime/common/headers.rs @@ -17,11 +17,11 @@ use std::collections::hash_map::IntoIter; use crate::body::mime::common::{ consume_crlf, data_copy, trim_front_lwsp, BytesResult, TokenResult, }; -use crate::body::mime::{CR, LF}; use crate::body::TokenStatus; use crate::error::{ErrorKind, HttpError}; use crate::h1::response::decoder::{HEADER_NAME_BYTES, HEADER_VALUE_BYTES}; use crate::headers::{HeaderName, HeaderValue, Headers}; +use crate::util::{CR, LF}; #[derive(Debug, PartialEq)] pub(crate) enum HeaderStatus { diff --git a/ylong_http/src/body/mime/common/mod.rs b/ylong_http/src/body/mime/common/mod.rs index 795a87950d5ff6728cfe2a7307dfd8bf6f68cad6..28de44b237db6da79cdaeb4c7798f836e108ef54 100644 --- a/ylong_http/src/body/mime/common/mod.rs +++ b/ylong_http/src/body/mime/common/mod.rs @@ -30,17 +30,7 @@ use std::io::Read; use crate::error::{ErrorKind, HttpError}; use crate::headers::Headers; - -// RFC5234 ABNF -// horizontal tab -pub(crate) const HTAB: u8 = b'\t'; -// 0x20 space -pub(crate) const SP: u8 = b' '; -// carriage return -pub(crate) const CR: u8 = b'\r'; -// linefeed -pub(crate) const LF: u8 = b'\n'; -pub(crate) const CRLF: &[u8] = b"\r\n"; +use crate::util::{trim_ascii, trim_ascii_start, CR, HTAB, LF, SP}; /// Represents component encoding/decoding status. #[derive(Debug, Eq, PartialEq)] @@ -153,39 +143,6 @@ pub(crate) fn get_crlf_contain(buf: &[u8]) -> TokenStatus<(&[u8], &[u8]), &[u8]> TokenStatus::Partial(buf) } -// TODO: Replace with `[u8]::trim_ascii_start` when is stable. -fn trim_ascii_start(mut bytes: &[u8]) -> &[u8] { - // Note: A pattern matching based approach (instead of indexing) allows - // making the function const. - while let [first, rest @ ..] = bytes { - if first.is_ascii_whitespace() { - bytes = rest; - } else { - break; - } - } - bytes -} - -// TODO: Replace with `[u8]::trim_ascii_end` when is stable. -fn trim_ascii_end(mut bytes: &[u8]) -> &[u8] { - // Note: A pattern matching based approach (instead of indexing) allows - // making the function const. - while let [rest @ .., last] = bytes { - if last.is_ascii_whitespace() { - bytes = rest; - } else { - break; - } - } - bytes -} - -// TODO: Replace with `[u8]::trim_ascii` when is stable. -fn trim_ascii(bytes: &[u8]) -> &[u8] { - trim_ascii_end(trim_ascii_start(bytes)) -} - // get multipart boundary pub(crate) fn get_content_type_boundary(headers: &Headers) -> Option> { let header_value = headers.get("Content-Type"); diff --git a/ylong_http/src/body/mime/common/part.rs b/ylong_http/src/body/mime/common/part.rs index 6c5e4df8e9e6aab1c614ce8774ce6909270091e0..0fceb84bcbcedc3292e72d21f6f4ac85c3b378df 100644 --- a/ylong_http/src/body/mime/common/part.rs +++ b/ylong_http/src/body/mime/common/part.rs @@ -15,9 +15,10 @@ use core::convert::TryFrom; use core::mem::take; use std::io::Read; -use crate::body::mime::{MixFrom, CR, LF}; +use crate::body::mime::MixFrom; use crate::error::HttpError; use crate::headers::{Header, HeaderName, HeaderValue, Headers}; +use crate::util::{CR, LF}; use crate::AsyncRead; /// `MimePart` is a body part of a Composite MIME body which is defined in diff --git a/ylong_http/src/body/mime/mimetype.rs b/ylong_http/src/body/mime/mimetype.rs index 0dfc9834db862749b6b45b33c269e59572cbd900..ed4a046d571d67c8e5de68b2fad0acdb59bffd91 100644 --- a/ylong_http/src/body/mime/mimetype.rs +++ b/ylong_http/src/body/mime/mimetype.rs @@ -15,6 +15,7 @@ use core::str; use std::path::Path; use crate::error::{ErrorKind, HttpError}; +use crate::util::SLASH; /// A type that defines the general structure of the `MIME` media typing system. /// @@ -67,7 +68,7 @@ impl<'a> MimeType<'a> { let (slash, _) = bytes .iter() .enumerate() - .find(|(_, &b)| b == b'/') + .find(|(_, &b)| b == SLASH) .ok_or_else(|| HttpError::from(ErrorKind::InvalidInput))?; let tag = MimeTypeTag::from_bytes(&bytes[..slash])?; diff --git a/ylong_http/src/body/mime/mod.rs b/ylong_http/src/body/mime/mod.rs index ea6d7c843126aad4b9c7a5dbf24edba46bf6cb3c..18a9b7fb7360abf78208873c609cdd5739550411 100644 --- a/ylong_http/src/body/mime/mod.rs +++ b/ylong_http/src/body/mime/mod.rs @@ -26,9 +26,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; use std::vec::IntoIter; -pub(crate) use common::{ - DecodeHeaders, EncodeHeaders, HeaderStatus, MixFrom, PartStatus, CR, CRLF, HTAB, LF, SP, -}; +pub(crate) use common::{DecodeHeaders, EncodeHeaders, HeaderStatus, MixFrom, PartStatus}; pub use common::{MimeMulti, MimeMultiBuilder, MimePart, MimePartBuilder, TokenStatus, XPart}; pub use decode::MimeMultiDecoder; pub(crate) use decode::MimePartDecoder; diff --git a/ylong_http/src/cookies/error.rs b/ylong_http/src/cookies/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1e4acfb3ee5c3bfceed26f2924751ca87676a43 --- /dev/null +++ b/ylong_http/src/cookies/error.rs @@ -0,0 +1,19 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Errors that may occur when using `cookies`. +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum CookieError { + InvalidPair, + InvalidName, +} diff --git a/ylong_http/src/cookies/mod.rs b/ylong_http/src/cookies/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..7d5ab699d2a574b9af58b6c11bee438e6b498294 --- /dev/null +++ b/ylong_http/src/cookies/mod.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TODO: The SameSite cookie attribute. +// HTTP draft: https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 + +mod error; +mod setcookie; +mod time; + +use core::{iter, slice}; + +pub(crate) use error::CookieError; +pub use setcookie::SetCookie; +use time::NormalTime; + +use crate::error::{ErrorKind, HttpError}; +use crate::headers::{HeaderValue, Headers}; + +const SET_COOKIE: &str = "set_cookie"; + +/// TODO: add doc. +pub fn cookies_from_headers( + headers: &Headers, +) -> impl Iterator, HttpError>> { + OpIter({ + let a = headers + .get(SET_COOKIE) + .map(|x| x.iter().map(|v| SetCookie::parse_whole(v))); + a + }) +} + +/// Uses `Option>` likes `I: Iterator`. +struct OpIter(Option); + +impl Iterator for OpIter +where + I: Iterator, +{ + type Item = R; + + fn next(&mut self) -> Option { + match &mut self.0 { + Some(i) => i.next(), + None => None, + } + } +} diff --git a/ylong_http/src/cookies/setcookie.rs b/ylong_http/src/cookies/setcookie.rs new file mode 100644 index 0000000000000000000000000000000000000000..b01ed935496094d381e245993d2f599867665124 --- /dev/null +++ b/ylong_http/src/cookies/setcookie.rs @@ -0,0 +1,362 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::borrow::Cow; +use std::time::Duration; + +use super::time::NormalTime; +use super::CookieError; +use crate::error::{ErrorKind, HttpError}; +use crate::util::{ + trim_ascii, trim_ascii_end, trim_ascii_start, COLON, EQUAL, FULLSTOP, HYPHEN, SEMICOLON, SLASH, +}; + +#[derive(Debug, Default)] +pub struct SetCookie<'a> { + cookie_string: Option>, + name: Option>, + value: Option>, + expiry: Option, + // seconds + max_age: Option, + domain: Option>, + path: Option>, + secure_only: Option, + http_only: Option, +} + +impl<'a> SetCookie<'a> { + fn new() -> SetCookie<'a> { + SetCookie { + cookie_string: None, + name: None, + value: None, + expiry: None, + max_age: None, + domain: None, + path: None, + secure_only: None, + http_only: None, + } + } + + /// The algorithm to parse a "set-cookie-string" is based on [`RFC 6265`]. + /// + /// [`RFC 6265`]: https://www.rfc-editor.org/rfc/rfc6265#section-5.2 + pub(crate) fn parse_whole(buf: &[u8]) -> Result { + let mut v = buf.split(|b| *b == SEMICOLON).map(trim_ascii); + let pair = v + .next() + .ok_or(ErrorKind::Cookie(CookieError::InvalidPair))?; + + let (name, value) = Self::parse_pair(pair)?; + if name.is_empty() { + return Err(ErrorKind::Cookie(CookieError::InvalidName).into()); + } + + let mut cookie: SetCookie = SetCookie::new(); + cookie.name = Some(Cow::from(name)); + cookie.value = Some(Cow::from(value)); + + if let Some(attr) = v.next() { + cookie.parse_attribute(attr)?; + } + + let s = Cow::from(buf); + cookie.cookie_string = Some(s); + Ok(cookie) + } + + /// Make sure `buf` is trimmed before using. + fn parse_pair(buf: &[u8]) -> Result<(&[u8], &[u8]), HttpError> { + match buf.iter().position(|b| *b == EQUAL) { + Some(i) => Ok((trim_ascii_end(&buf[..i]), trim_ascii_start(&buf[(i + 1)..]))), + None => Err(ErrorKind::Cookie(CookieError::InvalidPair).into()), + } + } + + /// Make sure `buf` is trimmed before using. + fn parse_attribute(&mut self, buf: &'a [u8]) -> Result<(), HttpError> { + let (key, value) = match buf.iter().position(|b| *b == EQUAL) { + Some(i) => ( + trim_ascii_end(&buf[..i]), + Some(trim_ascii_start(&buf[(i + 1)..])), + ), + None => (buf, None), + }; + + match (&*key.to_ascii_lowercase(), value) { + (b"expires", Some(v)) => self.parse_expires(v), + (b"max-age", Some(v)) => self.parse_max_age(v), + (b"domain", Some(v)) => self.parse_domain(v), + (b"path", Some(v)) => self.parse_path(v), + (b"secure", _) => self.secure_only = Some(true), + (b"httponly", _) => self.http_only = Some(true), + // nonstandard + _ => {} + } + + Ok(()) + } + + fn parse_expires(&mut self, buf: &'a [u8]) { + self.expiry = parse_date(buf); + } + + fn parse_max_age(&mut self, buf: &'a [u8]) { + let mut is_minus = false; + + let v = if !buf.is_empty() && buf[0] == HYPHEN { + is_minus = true; + &buf[1..] + } else { + buf + }; + + if !v.iter().all(|x| x.is_ascii_digit()) { + return; + } + + // In RFC 6265: If delta-seconds is less than or equal to zero (0), let + // expiry-time be the earliest representable date and time. + if is_minus { + self.max_age = Some(0); + } else { + // SAFE: The &[u8] is checked before using. + let s = unsafe { std::str::from_utf8_unchecked(v) }; + let i = s.parse::().unwrap_or(i64::MAX); + self.max_age = Some(i); + } + } + + fn parse_domain(&mut self, buf: &'a [u8]) { + if !buf.is_empty() { + let v = if buf[0] == FULLSTOP && buf.len() > 1 { + &buf[1..] + } else { + buf + }; + self.path = Some(Cow::from(v)) + } + } + + fn parse_path(&mut self, buf: &'a [u8]) { + if !buf.is_empty() && buf[0] == SLASH { + self.path = Some(Cow::from(buf)) + } + // else means default-path. + } +} + +/// The algorithm to parse a "cookie-date" is based on [`RFC 6265`]. +/// +/// [`RFC 6265`]: https://www.rfc-editor.org/rfc/rfc6265#section-5.1.1 +fn parse_date(buf: &[u8]) -> Option { + let mut found_time = false; + let mut found_day_of_month = false; + let mut found_month = false; + let mut found_year = false; + let mut date = NormalTime::new(); + + let date_token_list = parse_date_token_list(buf); + + for date_token in date_token_list { + if !found_time { + if let Some((h, m, s)) = parse_time(date_token) { + date.set_hour(h); + date.set_minute(m); + date.set_second(s); + found_time = true; + continue; + } + } + + if !found_day_of_month { + if let Some(d) = parse_day_of_month(date_token) { + date.set_day(d); + found_day_of_month = true; + continue; + } + } + + if !found_month { + if let Some(month) = parse_month(date_token) { + date.set_month(month); + found_month = true; + continue; + } + } + + if !found_year { + if let Some(y) = parse_year(date_token) { + date.set_year(y); + found_year = true; + continue; + } + } + } + + if found_time && found_day_of_month && found_month && found_year { + return Some(date); + } + + None +} + +fn parse_date_token_list(buf: &[u8]) -> Vec<&[u8]> { + buf.split(is_delimiter) + .filter(|v| !v.is_empty()) + .collect::>() +} + +// delimiter = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E +fn is_delimiter(b: &u8) -> bool { + *b == 0x09 + || (*b >= 0x20 && *b <= 0x2F) + || (*b >= 0x3B && *b <= 0x40) + || (*b >= 0x5B && *b <= 0x60) + || (*b >= 0x7B && *b <= 0x7E) +} + +// time = hms-time ( non-digit *OCTET ) +// hms-time = time-field ":" time-field ":" time-field +// time-field = 1*2DIGIT +fn parse_time(buf: &[u8]) -> Option<(i32, i32, i32)> { + let v = buf + .split(|b| *b == COLON) + .filter_map(|v| { + if let Ok(s) = std::str::from_utf8(v) { + if let Ok(num) = s.parse::() { + return Some(num); + } + } + None + }) + .collect::>(); + + if v.len() >= 3 { + let h = v[0]; + let m = v[1]; + let s = v[2]; + + if h < 24 && m < 60 && s < 60 { + return Some((h, m, s)); + } + } + None +} + +// day-of-month = 1*2DIGIT ( non-digit *OCTET ) +fn parse_day_of_month(buf: &[u8]) -> Option { + if let Ok(s) = std::str::from_utf8(buf) { + if let Ok(num) = s.parse::() { + if (1..=31).contains(&num) { + return Some(num); + } + } + } + None +} + +// month = ( "jan" / "feb" / "mar" / "apr" / +// "may" / "jun" / "jul" / "aug" / +// "sep" / "oct" / "nov" / "dec" ) *OCTET +fn parse_month(buf: &[u8]) -> Option { + if buf.len() < 3 { + return None; + } + + match &*buf[..3].to_ascii_lowercase() { + b"jan" => Some(1), + b"feb" => Some(2), + b"mar" => Some(3), + b"apr" => Some(4), + b"may" => Some(5), + b"jun" => Some(6), + b"jul" => Some(7), + b"aug" => Some(8), + b"sep" => Some(9), + b"oct" => Some(10), + b"nov" => Some(11), + b"dec" => Some(12), + _ => None, + } +} + +// year = 2*4DIGIT ( non-digit *OCTET ) +fn parse_year(buf: &[u8]) -> Option { + if let Ok(s) = std::str::from_utf8(buf) { + if let Ok(mut num) = s.parse::() { + if (0..=69).contains(&num) { + num += 2000; + } else if (70..=99).contains(&num) { + num += 1900; + } else if num < 1601 { + return None; + } + + return Some(num); + } + } + None +} + +#[cfg(test)] +mod ut_setcookie { + use crate::cookies::setcookie::{parse_day_of_month, parse_month, parse_time, parse_year}; + use crate::cookies::SetCookie; + + // TODO: add doc. + + #[test] + fn ut_parse_time() { + assert_eq!(parse_time(b""), None); + assert_eq!(parse_time(b"5:10"), None); + assert_eq!(parse_time(b"5:10:20"), Some((5, 10, 20))); + assert_eq!(parse_time(b"24:60:60"), None); + } + + #[test] + fn ut_parse_day_of_month() { + assert_eq!(parse_day_of_month(b""), None); + assert_eq!(parse_day_of_month(b"0"), None); + assert_eq!(parse_day_of_month(b"32"), None); + assert_eq!(parse_day_of_month(b"20"), Some(20)); + } + + #[test] + fn ut_parse_month() { + assert_eq!(parse_month(b"ja"), None); + assert_eq!(parse_month(b"jan"), Some(1)); + assert_eq!(parse_month(b"FEB"), Some(2)); + assert_eq!(parse_month(b"March"), Some(3)); + } + + #[test] + fn ut_parse_year() { + assert_eq!(parse_year(b"50"), Some(2050)); + assert_eq!(parse_year(b"80"), Some(1980)); + assert_eq!(parse_year(b"2023"), Some(2023)); + assert_eq!(parse_year(b"1500"), None); + } + + #[test] + fn ut_setcookie_parse_whole() { + assert!(SetCookie::parse_whole(b"bar").is_err()); + assert!(SetCookie::parse_whole(b"=bar").is_err()); + assert!(SetCookie::parse_whole(b" =bar").is_err()); + + assert!(SetCookie::parse_whole(b"foo=").is_ok()); + assert!(SetCookie::parse_whole(b" foo = bar").is_ok()); + } +} diff --git a/ylong_http/src/cookies/time.rs b/ylong_http/src/cookies/time.rs new file mode 100644 index 0000000000000000000000000000000000000000..1e9162454e01680e8c59a03f75e521669c89bbd0 --- /dev/null +++ b/ylong_http/src/cookies/time.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[derive(Debug)] +pub(crate) struct NormalTime { + year: i32, + month: i32, + day: i32, + hour: i32, + minute: i32, + second: i32, +} + +impl NormalTime { + pub(crate) fn new() -> Self { + NormalTime { + year: 0, + month: 0, + day: 0, + hour: 0, + minute: 0, + second: 0, + } + } + + pub(crate) fn set_year(&mut self, num: i32) { + self.year = num; + } + + pub(crate) fn set_month(&mut self, num: i32) { + self.month = num; + } + + pub(crate) fn set_day(&mut self, num: i32) { + self.day = num; + } + + pub(crate) fn set_hour(&mut self, num: i32) { + self.hour = num; + } + + pub(crate) fn set_minute(&mut self, num: i32) { + self.minute = num; + } + + pub(crate) fn set_second(&mut self, num: i32) { + self.second = num; + } +} diff --git a/ylong_http/src/error.rs b/ylong_http/src/error.rs index 1574e692c1f7b06a5b09aa53f549e494920f8ce8..cefd0ecab8fe0691e903e6c86f4eb8cc172e535c 100644 --- a/ylong_http/src/error.rs +++ b/ylong_http/src/error.rs @@ -24,6 +24,7 @@ use core::fmt::{Debug, Display, Formatter}; use std::convert::Infallible; use std::error::Error; +use crate::cookies::CookieError; #[cfg(feature = "http1_1")] use crate::h1::H1Error; #[cfg(feature = "http2")] @@ -84,4 +85,6 @@ pub(crate) enum ErrorKind { /// Errors related to `HTTP/2`. #[cfg(feature = "http2")] H2(H2Error), + + Cookie(CookieError), } diff --git a/ylong_http/src/lib.rs b/ylong_http/src/lib.rs index 9fac33a9539ede07c6b672522502f3b3d2856408..9cc4b60ea75d2d1eeeed9865f50c631d203aaa2b 100644 --- a/ylong_http/src/lib.rs +++ b/ylong_http/src/lib.rs @@ -34,7 +34,10 @@ pub mod h3; #[cfg(feature = "huffman")] mod huffman; +mod util; + pub mod body; +pub mod cookies; pub mod error; pub mod headers; pub mod request; diff --git a/ylong_http/src/response/mod.rs b/ylong_http/src/response/mod.rs index 1e4bfd8b2563fb09c9b5ea16550e8ac60db77a15..3299b3c8b4780f4e78a6257d93a5666da8313344 100644 --- a/ylong_http/src/response/mod.rs +++ b/ylong_http/src/response/mod.rs @@ -16,6 +16,7 @@ pub mod status; use status::StatusCode; +use crate::cookies::{cookies_from_headers, SetCookie}; use crate::headers::Headers; use crate::version::Version; @@ -75,6 +76,11 @@ impl Response { pub fn from_raw_parts(part: ResponsePart, body: T) -> Response { Self { part, body } } + + /// TODO: adds doc. + pub fn cookies(&self) -> impl Iterator { + cookies_from_headers(self.headers()).filter_map(Result::ok) + } } impl Clone for Response { diff --git a/ylong_http/src/util.rs b/ylong_http/src/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..6f9bc01a1428a18dd9fab111a1b8258fae9e110c --- /dev/null +++ b/ylong_http/src/util.rs @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// RFC5234 ABNF +/// 0x09, horizontal tab +pub(crate) const HTAB: u8 = b'\t'; +/// 0x0A, linefeed +pub(crate) const LF: u8 = b'\n'; +/// 0x0D, carriage return +pub(crate) const CR: u8 = b'\r'; +/// 0x20, space +pub(crate) const SP: u8 = b' '; +pub(crate) const CRLF: &[u8] = b"\r\n"; + +/// 0x2D, `-` +pub(crate) const HYPHEN: u8 = b'-'; +/// 0x2E, `.` +pub(crate) const FULLSTOP: u8 = b'.'; +/// 0x2F, `/` +pub(crate) const SLASH: u8 = b'/'; +/// 0x3A, `:` +pub(crate) const COLON: u8 = b':'; +/// 0x3B, `;` +pub(crate) const SEMICOLON: u8 = b';'; +/// 0x3D, `=` +pub(crate) const EQUAL: u8 = b'='; + +// TODO: Replace with `[u8]::trim_ascii_start` when is stable. +pub(crate) fn trim_ascii_start(mut bytes: &[u8]) -> &[u8] { + // Note: A pattern matching based approach (instead of indexing) allows + // making the function const. + while let [first, rest @ ..] = bytes { + if first.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + bytes +} + +// TODO: Replace with `[u8]::trim_ascii_end` when is stable. +pub(crate) fn trim_ascii_end(mut bytes: &[u8]) -> &[u8] { + // Note: A pattern matching based approach (instead of indexing) allows + // making the function const. + while let [rest @ .., last] = bytes { + if last.is_ascii_whitespace() { + bytes = rest; + } else { + break; + } + } + bytes +} + +// TODO: Replace with `[u8]::trim_ascii` when is stable. +pub(crate) fn trim_ascii(bytes: &[u8]) -> &[u8] { + trim_ascii_end(trim_ascii_start(bytes)) +} diff --git a/ylong_http_client/src/async_impl/client.rs b/ylong_http_client/src/async_impl/client.rs index 6b4f6e472ff4b3018611300c2da8f1988166af51..bb36b94f0dc4b51b26a7c1ce1f22b6450003f946 100644 --- a/ylong_http_client/src/async_impl/client.rs +++ b/ylong_http_client/src/async_impl/client.rs @@ -11,11 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + use ylong_http::body::{ChunkBody, TextBody}; +use ylong_http::cookies::cookies_from_headers; +use ylong_http::request::RequestPart; use ylong_http::response::Response; use super::{conn, Body, ConnPool, Connector, HttpBody, HttpConnector}; use crate::async_impl::timeout::TimeoutFuture; +use crate::util::cookies::EasyCookie; use crate::util::normalizer::{RequestFormatter, UriFormatter}; use crate::util::proxy::Proxies; use crate::util::redirect::TriggerKind; @@ -118,7 +123,7 @@ impl Client { &self, request: Request, ) -> Result { - let (part, body) = request.into_parts(); + let (mut part, body) = request.into_parts(); let content_length = part .headers @@ -134,6 +139,9 @@ impl Client { .map(|v| v.contains("chunked")) .unwrap_or(false); + self.check_cookie_store(&mut part); + let uri = part.uri.clone(); + let response = match (content_length, transfer_encoding) { (_, true) => { let request = Request::from_raw_parts(part, ChunkBody::from_async_body(body)); @@ -148,6 +156,14 @@ impl Client { self.retry_send_request(request).await } }; + + if let Ok(ref res) = response { + if let Some(ref cookie_store) = self.client_config.cookie_store { + let mut cookies = cookies_from_headers(res.headers()).filter_map(Result::ok); + cookie_store.set_cookies(&mut cookies, &uri); + } + } + response.map(super::Response::new) } @@ -274,6 +290,16 @@ impl Client { return response; } } + + fn check_cookie_store(&self, part: &mut RequestPart) { + if let Some(cookie_store) = self.client_config.cookie_store.as_ref() { + if part.headers.get("cookie").is_none() { + if let Some(header) = cookie_store.cookies(&part.uri) { + let _ = part.headers.insert("cookie", header); + } + } + } + } } impl Default for Client { @@ -459,6 +485,25 @@ impl ClientBuilder { self } + /// TODO: add doc. + pub fn cookie_default(mut self, is_use: bool) -> ClientBuilder { + if is_use { + self = self.cookie_store(Arc::new(EasyCookie::default())); + } else { + self.client.cookie_store = None; + } + self + } + + /// TODO: add doc. + pub fn cookie_store( + mut self, + cookie_store: Arc, + ) -> ClientBuilder { + self.client.cookie_store = Some(cookie_store); + self + } + /// Constructs a `Client` based on the given settings. /// /// # Examples diff --git a/ylong_http_client/src/util/config/client.rs b/ylong_http_client/src/util/config/client.rs index a275b5dff395c70489b360087c1c5aee43e01cd1..2b631b08594ffc3e84ff166cae9f0765f454b577 100644 --- a/ylong_http_client/src/util/config/client.rs +++ b/ylong_http_client/src/util/config/client.rs @@ -13,6 +13,9 @@ //! Client configure module. +use std::sync::Arc; + +use crate::util::cookies::CookieStore; use crate::util::{Redirect, Retry, Timeout}; /// Options and flags which can be used to configure a client. @@ -21,6 +24,7 @@ pub(crate) struct ClientConfig { pub(crate) retry: Retry, pub(crate) connect_timeout: Timeout, pub(crate) request_timeout: Timeout, + pub(crate) cookie_store: Option>, } impl ClientConfig { @@ -31,6 +35,7 @@ impl ClientConfig { retry: Retry::none(), connect_timeout: Timeout::none(), request_timeout: Timeout::none(), + cookie_store: None, } } } diff --git a/ylong_http_client/src/util/cookies/cookie.rs b/ylong_http_client/src/util/cookies/cookie.rs new file mode 100644 index 0000000000000000000000000000000000000000..e956d6f8bfbc30adbb7b974332f5bd49667da5dc --- /dev/null +++ b/ylong_http_client/src/util/cookies/cookie.rs @@ -0,0 +1,47 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Client configure module. + +use std::borrow::Cow; + +use ylong_http::cookies::SetCookie; +use ylong_http::headers::HeaderValue; +use ylong_http::request::uri::Uri; + +pub trait CookieStore: Send + Sync { + fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &Uri); + fn cookies(&self, url: &Uri) -> Option; +} + +#[derive(Debug, Default)] +pub struct EasyCookie { + inner: (), +} + +impl CookieStore for EasyCookie { + fn set_cookies( + &self, + _cookie_headers: &mut dyn Iterator>, + _url: &ylong_http::request::uri::Uri, + ) { + todo!() + } + + fn cookies( + &self, + _url: &ylong_http::request::uri::Uri, + ) -> Option { + todo!() + } +} diff --git a/ylong_http_client/src/util/cookies/mod.rs b/ylong_http_client/src/util/cookies/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..2c8b1843a9e069328d0dcad1b621f132c3c847b7 --- /dev/null +++ b/ylong_http_client/src/util/cookies/mod.rs @@ -0,0 +1,19 @@ +// Copyright (c) 2023 Huawei Device Co., Ltd. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Client configure module. + +mod cookie; + +pub use cookie::CookieStore; +pub(crate) use cookie::EasyCookie; diff --git a/ylong_http_client/src/util/mod.rs b/ylong_http_client/src/util/mod.rs index 0dd45fe422eab48827a9415eb66112437db3d58f..b8aaab0d2716a105c3e676deb1651c6bcec4abad 100644 --- a/ylong_http_client/src/util/mod.rs +++ b/ylong_http_client/src/util/mod.rs @@ -23,6 +23,8 @@ #![allow(unused_imports)] mod config; +#[cfg(feature = "http2")] +pub use config::H2Config; #[cfg(feature = "__tls")] pub use config::{AlpnProtocol, AlpnProtocolList}; pub(crate) use config::{ClientConfig, ConnectorConfig, HttpConfig, HttpVersion}; @@ -32,8 +34,6 @@ pub use config::{Proxy, ProxyBuilder, Redirect, Retry, SpeedLimit, Timeout}; pub(crate) mod c_openssl; #[cfg(feature = "__c_openssl")] pub use c_openssl::{Cert, Certificate, TlsConfig, TlsConfigBuilder, TlsFileType, TlsVersion}; -#[cfg(feature = "http2")] -pub use config::H2Config; #[cfg(any(feature = "http1_1", feature = "http2"))] pub(crate) mod dispatcher; @@ -42,5 +42,6 @@ pub(crate) mod normalizer; pub(crate) mod pool; pub(crate) mod base64; +pub(crate) mod cookies; pub(crate) mod proxy; pub(crate) mod redirect;