From c283a683859e46fc36a93bc0c0f2b4bbf974b02f Mon Sep 17 00:00:00 2001 From: kanarus Date: Wed, 12 Feb 2025 22:23:09 +0900 Subject: [PATCH 01/17] 2025-02-12 22:23+9:00 --- ohkami/src/fang/builtin.rs | 3 +++ ohkami/src/fang/builtin/helmet.rs | 41 +++++++++++++++++++++++++++++++ ohkami/src/response/headers.rs | 1 + 3 files changed, 45 insertions(+) create mode 100644 ohkami/src/fang/builtin/helmet.rs diff --git a/ohkami/src/fang/builtin.rs b/ohkami/src/fang/builtin.rs index bc700c2a9..1787f260a 100644 --- a/ohkami/src/fang/builtin.rs +++ b/ohkami/src/fang/builtin.rs @@ -10,6 +10,9 @@ pub use jwt::{JWT, JWTToken}; mod context; pub use context::Context; +mod helmet; +pub use helmet::Helmet; + #[cfg(feature="__rt_native__")] mod timeout; #[cfg(feature="__rt_native__")] diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs new file mode 100644 index 000000000..4731d77d0 --- /dev/null +++ b/ohkami/src/fang/builtin/helmet.rs @@ -0,0 +1,41 @@ +pub struct Helmet(Option>); + +struct HelmetFields { + delete_x_powered_by: bool, + content_security_policy: Option, +} + +impl Default for Helmet { + fn default() -> Self { + Self(Some(Box::new(HelmetFields { + delete_x_powered_by: true, + content_security_policy: None, + }))) + } +} + +impl Helmet { + pub fn delete_XPoweredBy(mut self, yes: bool) -> Self { + self.delete_XPoweredBy = yes; + self + } + pub fn ContentSecurityPolicy(mut self, setter: impl FnOnce(field::ContentSecurityPolicy) -> field::ContentSecurityPolicy) -> Self { + self.ContentSecurityPolicy = setter(ContentSecurityPolicy(String::new())); + self + } + pub fn ContentSecurityPolicyReportOnly(mut self, setter: impl FnOnce(field::ContentSecurityPolicy) -> field::ContentSecurityPolicy) -> Self { + self.ContentSecurityPolicyReportOnly = setter(ContentSecurityPolicyReportOnly(String::new())); + self + } + pub fn CrossOriginEmbedderPolicy_require_corp(mut self) -> Self { + self.CrossOriginEmbedderPolicy_require_corp = true; + self + } +} + +mod field { + struct ContentSecurityPolicy(String); + impl ContentSecurityPolicy { + + } +} diff --git a/ohkami/src/response/headers.rs b/ohkami/src/response/headers.rs index 739218b58..5ff2543fb 100644 --- a/ohkami/src/response/headers.rs +++ b/ohkami/src/response/headers.rs @@ -212,6 +212,7 @@ macro_rules! Header { ContentSecurityPolicy: b"Content-Security-Policy", ContentSecurityPolicyReportOnly: b"Content-Security-Policy-Report-Only", ContentType: b"Content-Type", + CrossOriginEmbedderPolicy: b"Cross-Origin-Embedder-Policy", Date: b"Date", ETag: b"ETag", Expires: b"Expires", From 59caa24beba5f22dbf9d391ea6956809afec3bb6 Mon Sep 17 00:00:00 2001 From: kanarus Date: Thu, 13 Feb 2025 16:40:37 +0900 Subject: [PATCH 02/17] 2025-02-13 16:40+9:00 --- ohkami/src/fang/builtin/helmet.rs | 91 +++++++++++++++++++++++++------ ohkami/src/response/headers.rs | 1 + 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index 4731d77d0..5e7d8b9d0 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -1,30 +1,87 @@ -pub struct Helmet(Option>); +pub struct Helmet(Box); +/// based on , +/// with removing non-standard or deprecated headers +#[derive(Clone)] struct HelmetFields { - delete_x_powered_by: bool, - content_security_policy: Option, + pub ContentSecurityPolicy: Option, + pub ContentSecurityPolicyReportOnly: Option, + pub CrossOriginEmbedderPolicy: Option<&'static str>, + pub CrossOriginResourcePolicy: Option<&'static str>, + pub ReferrerPolicy: Option<&'static str>, + pub StrictTransportSecurity: Option<&'static str>, + pub XContentTypeOptions: Option<&'static str>, + pub XFrameOptions: Option<&'static str>, } -impl Default for Helmet { - fn default() -> Self { - Self(Some(Box::new(HelmetFields { - delete_x_powered_by: true, - content_security_policy: None, - }))) - } +/// based on +#[derive(Clone)] +pub struct CSP { + pub default_src: Option<&'static str>, + pub script_src: Option<&'static str>, + pub style_src: Option<&'static str>, + pub img_src: Option<&'static str>, + pub connect_src: Option<&'static str>, + pub font_src: Option<&'static str>, + pub object_src: Option<&'static str>, + pub media_src: Option<&'static str>, + pub frame_src: Option<&'static str>, + pub sandbox: Option, + pub report_uri: Option<&'static str>, + pub child_src: Option<&'static str>, + pub form_action: Option<&'static str>, + pub frame_ancestors: Option<&'static str>, + pub plugin_types: Option<&'static str>, + pub base_uri: Option<&'static str>, + pub report_to: Option<&'static str>, + pub worker_src: Option<&'static str>, + pub manifest_src: Option<&'static str>, + pub prefetch_src: Option<&'static str>, + pub navifate_to: Option<&'static str>, + pub require_trusted_types_for: Option<&'static str>, + pub trusted_types: Option<&'static str>, + pub upgrade_insecure_requests: Option<&'static str>, + pub block_all_mixed_content: Option<&'static str>, } -impl Helmet { - pub fn delete_XPoweredBy(mut self, yes: bool) -> Self { - self.delete_XPoweredBy = yes; - self +const _: () = { + use crate::{Request, Response, Fang, FangProc}; + use std::sync::OnceLock; + + impl Fang for Helmet { + type Proc = HelmetProc; + fn chain(&self, inner: Inner) -> Self::Proc { + static SET_HEADERS: OnceLock>; + + /* clone only once */ + let set_headers = SET_HEADERS.get_or_init({ + + || { + + } + }); + + HelmetProc { inner, set_headers } + } + } + + struct HelmetProc { + set_headers: Box, + inner: I, + } + + impl FangProc for HelmetProc { + } +}; + +impl Helmet { pub fn ContentSecurityPolicy(mut self, setter: impl FnOnce(field::ContentSecurityPolicy) -> field::ContentSecurityPolicy) -> Self { - self.ContentSecurityPolicy = setter(ContentSecurityPolicy(String::new())); + self.ContentSecurityPolicy = Some(setter(ContentSecurityPolicy(String::new())).0); self } pub fn ContentSecurityPolicyReportOnly(mut self, setter: impl FnOnce(field::ContentSecurityPolicy) -> field::ContentSecurityPolicy) -> Self { - self.ContentSecurityPolicyReportOnly = setter(ContentSecurityPolicyReportOnly(String::new())); + self.ContentSecurityPolicyReportOnly = Some(setter(ContentSecurityPolicyReportOnly(String::new())).0); self } pub fn CrossOriginEmbedderPolicy_require_corp(mut self) -> Self { @@ -38,4 +95,6 @@ mod field { impl ContentSecurityPolicy { } + + } diff --git a/ohkami/src/response/headers.rs b/ohkami/src/response/headers.rs index 5ff2543fb..c8cfa7ff8 100644 --- a/ohkami/src/response/headers.rs +++ b/ohkami/src/response/headers.rs @@ -213,6 +213,7 @@ macro_rules! Header { ContentSecurityPolicyReportOnly: b"Content-Security-Policy-Report-Only", ContentType: b"Content-Type", CrossOriginEmbedderPolicy: b"Cross-Origin-Embedder-Policy", + CrossOriginResourcePolicy: b"Cross-Origin-Resource-Policy", Date: b"Date", ETag: b"ETag", Expires: b"Expires", From c90c8f2233a0ae211da56d5852437c9ec0a69f93 Mon Sep 17 00:00:00 2001 From: kanarus Date: Thu, 13 Feb 2025 17:05:58 +0900 Subject: [PATCH 03/17] 2025-02-13 17:05+9:00 --- ohkami/src/fang/builtin/helmet.rs | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index 5e7d8b9d0..90159c2a8 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -44,6 +44,66 @@ pub struct CSP { pub block_all_mixed_content: Option<&'static str>, } +/// ## Example +/// +/// ``` +/// use ohkami::prelude::*; +/// use ohkami::fang::{Helmet, Sandbox}; +/// +/// #[tokio::main] +/// async fn main() { +/// Ohkami::new(( +/// Helmet { +/// sandbox: Sandbox::allow_forms | Sandbox::allow_same_origin, +/// ..Default::default() +/// }, +/// "/hello".GET(|| async {"Hello, helmet!"}) +/// )).howl("localhost:3000").await +/// } +/// ``` +#[derive(Clone)] +pub struct Sandbox(u16); +const _: () = { + #[allow(non_upper_case_globals)] + impl Sandbox { + pub const allow_forms = Self(0b0000000001u16), + pub const allow_same_origin = Self(0b0000000010u16), + pub const allow_scripts = Self(0b0000000100u16), + pub const allow_popups = Self(0b0000001000u16), + pub const allow_modals = Self(0b0000010000u16), + pub const allow_orientation_lock = Self(0b0000100000u16), + pub const allow_pointer_lock = Self(0b0001000000u16), + pub const allow_presentation = Self(0b0010000000u16), + pub const allow_popups_to_escape_sandbox = Self(0b0100000000u16), + pub const allow_top_navigation = Self(0b1000000000u16), + } + + impl std::ops::BitOr for Sandbox { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } + } + + impl Sandbox { + pub(self) fn build(&self) -> String { + let mut result = String::from("sandbox"); + if self.0 & Self::allow_forms.0 != 0 {result.push_str(" allow-forms");} + if self.0 & Self::allow_same_origin.0 != 0 {result.push_str(" allow-same-origin");} + if self.0 & Self::allow_scripts.0 != 0 {result.push_str(" allow-scripts");} + if self.0 & Self::allow_popups.0 != 0 {result.push_str(" allow-popups");} + if self.0 & Self::allow_modals.0 != 0 {result.push_str(" allow-modals");} + if self.0 & Self::allow_orientation_lock.0 != 0 {result.push_str(" allow-orientation-lock");} + if self.0 & Self::allow_pointer_lock.0 != 0 {result.push_str(" allow-pointer-lock");} + if self.0 & Self::allow_presentation.0 != 0 {result.push_str(" allow-presentation");} + if self.0 & Self::allow_popups_to_escape_sandbox.0 != 0 {result.push_str(" allow-popups-to-escape-sandbox");} + if self.0 & Self::allow_top_navigation.0 != 0 {result.push_str(" allow-top-navigation");} + result + } + } +}; + const _: () = { use crate::{Request, Response, Fang, FangProc}; use std::sync::OnceLock; From ad64bd84f3f00554774b212ccbe5687621b08a83 Mon Sep 17 00:00:00 2001 From: kanarus Date: Thu, 13 Feb 2025 17:15:10 +0900 Subject: [PATCH 04/17] 2025-02-13 17:15+9:00 --- ohkami/src/fang/builtin/helmet.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index 90159c2a8..983749093 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -48,13 +48,13 @@ pub struct CSP { /// /// ``` /// use ohkami::prelude::*; -/// use ohkami::fang::{Helmet, Sandbox}; +/// use ohkami::fang::{Helmet, Sandbox::{allow_forms, allow_same_origin}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( /// Helmet { -/// sandbox: Sandbox::allow_forms | Sandbox::allow_same_origin, +/// sandbox: allow_forms | allow_same_origin, /// ..Default::default() /// }, /// "/hello".GET(|| async {"Hello, helmet!"}) @@ -104,6 +104,17 @@ const _: () = { } }; +pub struct SourceList { + directive: u16, + value: Option>, +} +const _: () = { + #[allow(non_upper_case_globals)] + impl SourceList { + + } +}; + const _: () = { use crate::{Request, Response, Fang, FangProc}; use std::sync::OnceLock; From af900d10914846a9abc480e51a79cc91dd1244b7 Mon Sep 17 00:00:00 2001 From: kanarus Date: Thu, 13 Feb 2025 22:33:38 +0900 Subject: [PATCH 05/17] skelton --- ohkami/src/fang/builtin/helmet.rs | 376 +++++++++++++++++++++++------- ohkami/src/response/headers.rs | 2 +- 2 files changed, 292 insertions(+), 86 deletions(-) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index 983749093..21588dbc0 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -1,48 +1,145 @@ -pub struct Helmet(Box); - /// based on , /// with removing non-standard or deprecated headers #[derive(Clone)] -struct HelmetFields { +#[allow(non_snake_case)] +pub struct Helmet { pub ContentSecurityPolicy: Option, pub ContentSecurityPolicyReportOnly: Option, - pub CrossOriginEmbedderPolicy: Option<&'static str>, - pub CrossOriginResourcePolicy: Option<&'static str>, - pub ReferrerPolicy: Option<&'static str>, - pub StrictTransportSecurity: Option<&'static str>, - pub XContentTypeOptions: Option<&'static str>, - pub XFrameOptions: Option<&'static str>, + pub CrossOriginEmbedderPolicy: &'static str, + pub CrossOriginResourcePolicy: &'static str, + pub ReferrerPolicy: &'static str, + pub StrictTransportSecurity: &'static str, + pub XContentTypeOptions: &'static str, + pub XFrameOptions: &'static str, } +const _: () = { + impl Default for Helmet { + fn default() -> Self { + Helmet { + ContentSecurityPolicy: None, + ContentSecurityPolicyReportOnly: None, + CrossOriginEmbedderPolicy: "require-corp", + CrossOriginResourcePolicy: "same-origin", + ReferrerPolicy: "no-referrer", + StrictTransportSecurity: "max-age=15552000; includeSubDomains", + XContentTypeOptions: "nosniff", + XFrameOptions: "SAMEORIGIN", + } + } + } + + impl Helmet { + pub(self) fn apply(&self, res: &mut crate::Response) { + let mut h = res.headers.set(); + if let Some(csp) = &self.ContentSecurityPolicy { + h = h.ContentSecurityPolicy(csp.build()); + } + if let Some(csp) = &self.ContentSecurityPolicyReportOnly { + h = h.ContentSecurityPolicyReportOnly(csp.build()); + } + if !self.CrossOriginEmbedderPolicy.is_empty() { + h = h.CrossOriginEmbedderPolicy(self.CrossOriginEmbedderPolicy); + } + if !self.CrossOriginResourcePolicy.is_empty() { + h = h.CrossOriginResourcePolicy(self.CrossOriginResourcePolicy); + } + if !self.ReferrerPolicy.is_empty() { + h = h.ReferrerPolicy(self.ReferrerPolicy); + } + if !self.StrictTransportSecurity.is_empty() { + h = h.StrictTransportSecurity(self.StrictTransportSecurity); + } + if !self.XContentTypeOptions.is_empty() { + h = h.XContentTypeOptions(self.XContentTypeOptions); + } + if !self.XFrameOptions.is_empty() { + h.XFrameOptions(self.XFrameOptions); + } + } + } +}; /// based on -#[derive(Clone)] +#[derive(Clone, Default)] pub struct CSP { - pub default_src: Option<&'static str>, - pub script_src: Option<&'static str>, - pub style_src: Option<&'static str>, - pub img_src: Option<&'static str>, - pub connect_src: Option<&'static str>, - pub font_src: Option<&'static str>, - pub object_src: Option<&'static str>, - pub media_src: Option<&'static str>, - pub frame_src: Option<&'static str>, - pub sandbox: Option, - pub report_uri: Option<&'static str>, - pub child_src: Option<&'static str>, - pub form_action: Option<&'static str>, - pub frame_ancestors: Option<&'static str>, - pub plugin_types: Option<&'static str>, - pub base_uri: Option<&'static str>, - pub report_to: Option<&'static str>, - pub worker_src: Option<&'static str>, - pub manifest_src: Option<&'static str>, - pub prefetch_src: Option<&'static str>, - pub navifate_to: Option<&'static str>, - pub require_trusted_types_for: Option<&'static str>, - pub trusted_types: Option<&'static str>, - pub upgrade_insecure_requests: Option<&'static str>, - pub block_all_mixed_content: Option<&'static str>, + pub default_src: SourceList, + pub script_src: SourceList, + pub style_src: SourceList, + pub img_src: SourceList, + pub connect_src: SourceList, + pub font_src: SourceList, + pub object_src: SourceList, + pub media_src: SourceList, + pub frame_src: SourceList, + pub sandbox: Sandbox, + pub report_uri: &'static str, + pub child_src: SourceList, + pub form_action: &'static str, + pub frame_ancestors: &'static str, + pub plugin_types: &'static str, + pub base_uri: &'static str, + pub report_to: &'static str, + pub worker_src: SourceList, + pub manifest_src: SourceList, + pub prefetch_src: SourceList, + pub navifate_to: &'static str, + pub require_trusted_types_for: &'static str, + pub trusted_types: &'static str, + pub upgrade_insecure_requests: &'static str, + pub block_all_mixed_content: &'static str, } +const _: () = { + impl CSP { + pub(self) fn build(&self) -> String { + let mut result = String::new(); + + macro_rules! append { + ($field:ident build as $policy:literal) => { + if !(self.$field.is_empty()) { + result.push_str(concat!($policy, " ")); + result.push_str(&*self.$field.build()); + result.push(';'); + } + }; + ($field:ident as $policy:literal) => { + if !(self.$field.is_empty()) { + result.push_str(concat!($policy, " ")); + result.push_str(self.$field); + result.push(';'); + } + }; + } + + append!(default_src build as "default-src"); + append!(script_src build as "script-src"); + append!(style_src build as "style-src"); + append!(img_src build as "img-src"); + append!(connect_src build as "connect-src"); + append!(font_src build as "font-src"); + append!(object_src build as "object-src"); + append!(media_src build as "media-src"); + append!(frame_src build as "frame-src"); + append!(sandbox build as "sandbox"); + append!(report_uri as "report-uri"); + append!(child_src build as "child-src"); + append!(form_action as "form-action"); + append!(frame_ancestors as "frame-ancestors"); + append!(plugin_types as "plugin-types"); + append!(base_uri as "base-uri"); + append!(report_to as "report-to"); + append!(worker_src build as "worker-src"); + append!(manifest_src build as "manifest-src"); + append!(prefetch_src build as "prefetch_src"); + append!(navifate_to as "navifate-to"); + append!(require_trusted_types_for as "require-trusted-types-for"); + append!(trusted_types as "trusted-types"); + append!(upgrade_insecure_requests as "upgrade-insecure-requests"); + append!(block_all_mixed_content as "block-all-mixed-content"); + + result + } + } +}; /// ## Example /// @@ -66,16 +163,16 @@ pub struct Sandbox(u16); const _: () = { #[allow(non_upper_case_globals)] impl Sandbox { - pub const allow_forms = Self(0b0000000001u16), - pub const allow_same_origin = Self(0b0000000010u16), - pub const allow_scripts = Self(0b0000000100u16), - pub const allow_popups = Self(0b0000001000u16), - pub const allow_modals = Self(0b0000010000u16), - pub const allow_orientation_lock = Self(0b0000100000u16), - pub const allow_pointer_lock = Self(0b0001000000u16), - pub const allow_presentation = Self(0b0010000000u16), - pub const allow_popups_to_escape_sandbox = Self(0b0100000000u16), - pub const allow_top_navigation = Self(0b1000000000u16), + pub const allow_forms: Self = Self(0b0000000001u16); + pub const allow_same_origin: Self = Self(0b0000000010u16); + pub const allow_scripts: Self = Self(0b0000000100u16); + pub const allow_popups: Self = Self(0b0000001000u16); + pub const allow_modals: Self = Self(0b0000010000u16); + pub const allow_orientation_lock: Self = Self(0b0000100000u16); + pub const allow_pointer_lock: Self = Self(0b0001000000u16); + pub const allow_presentation: Self = Self(0b0010000000u16); + pub const allow_popups_to_escape_sandbox: Self = Self(0b0100000000u16); + pub const allow_top_navigation: Self = Self(0b1000000000u16); } impl std::ops::BitOr for Sandbox { @@ -86,9 +183,19 @@ const _: () = { } } + impl Default for Sandbox { + fn default() -> Self { + Self(0b0000000000u16) + } + } + impl Sandbox { + pub(self) const fn is_empty(&self) -> bool { + self.0 == 0b0000000000u16 + } + pub(self) fn build(&self) -> String { - let mut result = String::from("sandbox"); + let mut result = String::new(); if self.0 & Self::allow_forms.0 != 0 {result.push_str(" allow-forms");} if self.0 & Self::allow_same_origin.0 != 0 {result.push_str(" allow-same-origin");} if self.0 & Self::allow_scripts.0 != 0 {result.push_str(" allow-scripts");} @@ -104,14 +211,133 @@ const _: () = { } }; +/// ## Example +/// +/// ``` +/// use ohkami::prelude::*; +/// use ohkami::fang::{Helmet, SouceList::{self_origin, data}}; +/// +/// #[tokio::main] +/// async fn main() { +/// Ohkami::new(( +/// Helmet { +/// script_src: self_origin | data, +/// ..Default::default() +/// }, +/// "/hello".GET(|| async {"Hello, helmet!"}) +/// )).howl("localhost:3000").await +/// } +/// ``` +#[derive(Clone, Default)] pub struct SourceList { - directive: u16, - value: Option>, + this: std::borrow::Cow<'static, str>, + list: Vec>, } const _: () = { + #[derive(Clone)] + #[allow(non_camel_case_types)] + pub enum Source { + any, + data, + https, + none, + self_origin, + strict_dynamic, + unsafe_inline, + unsafe_eval, + unsafe_hashes, + domain(&'static str), + sha256(String), + sha384(String), + sha512(String), + nonce(String), + } + impl Source { + pub(self) const fn build_const(&self) -> std::borrow::Cow<'static, str> { + match self { + Self::any => std::borrow::Cow::Borrowed("*"), + Self::data => std::borrow::Cow::Borrowed("data:"), + Self::https => std::borrow::Cow::Borrowed("https:"), + Self::none => std::borrow::Cow::Borrowed("'none'"), + Self::self_origin => std::borrow::Cow::Borrowed("'self'"), + Self::strict_dynamic => std::borrow::Cow::Borrowed("'strict-dynamic'"), + Self::unsafe_inline => std::borrow::Cow::Borrowed("'unsafe-inline'"), + Self::unsafe_eval => std::borrow::Cow::Borrowed("'unsafe-eval'"), + Self::unsafe_hashes => std::borrow::Cow::Borrowed("'unsafe-hashes'"), + Self::domain(s) => std::borrow::Cow::Borrowed(*s), + Self::sha256(_) => unreachable!(), + Self::sha384(_) => unreachable!(), + Self::sha512(_) => unreachable!(), + Self::nonce(_) => unreachable!(), + } + } + pub(self) fn build_hash(&self) -> std::borrow::Cow<'static, str> { + match self { + Self::any => unreachable!(), + Self::data => unreachable!(), + Self::https => unreachable!(), + Self::none => unreachable!(), + Self::self_origin => unreachable!(), + Self::strict_dynamic => unreachable!(), + Self::unsafe_inline => unreachable!(), + Self::unsafe_eval => unreachable!(), + Self::unsafe_hashes => unreachable!(), + Self::domain(_) => unreachable!(), + Self::sha256(s) => std::borrow::Cow::Owned(format!("'sha256-{s}'")), + Self::sha384(s) => std::borrow::Cow::Owned(format!("'sha384-{s}'")), + Self::sha512(s) => std::borrow::Cow::Owned(format!("'sha512-{s}'")), + Self::nonce(s) => std::borrow::Cow::Owned(format!("'nonce-{s}'")), + } + } + } + + macro_rules! this { + (const $src:expr) => {SourceList { this: $src.build_const(), list: Vec::new() }}; + (hash $src:expr) => {SourceList { this: $src.build_hash(), list: Vec::new() }}; + } #[allow(non_upper_case_globals)] impl SourceList { - + pub const any: Self = this!(const Source::any); + pub const data: Self = this!(const Source::data); + pub const https: Self = this!(const Source::https); + pub const none: Self = this!(const Source::none); + pub const self_origin: Self = this!(const Source::self_origin); + pub const strict_dynamic: Self = this!(const Source::strict_dynamic); + pub const unsafe_inline: Self = this!(const Source::unsafe_inline); + pub const unsafe_eval: Self = this!(const Source::unsafe_eval); + pub const unsafe_hashes: Self = this!(const Source::unsafe_hashes); + pub fn domain(domain: &'static str) -> Self {this!(const Source::domain(domain))} + pub fn sha256(sha256: String) -> Self {this!(hash Source::sha256(sha256))} + pub fn sha384(sha384: String) -> Self {this!(hash Source::sha384(sha384))} + pub fn sha512(sha512: String) -> Self {this!(hash Source::sha512(sha512))} + pub fn nonce (nonce: String) -> Self {this!(hash Source::nonce(nonce))} + } + + impl std::ops::BitOr for SourceList { + type Output = Self; + + fn bitor(mut self, rhs: Self) -> Self::Output { + self.list.push(rhs.this); + self.list.extend(rhs.list); + self + } + } + + impl SourceList { + pub(self) fn is_empty(&self) -> bool { + self.this.is_empty() + } + + pub(self) fn build(&self) -> String { + let mut result = String::from(&*self.this); + if !self.list.is_empty() { + for s in &self.list { + result.push(' '); + result.push_str(&*s); + } + } + result + } } }; @@ -119,53 +345,33 @@ const _: () = { use crate::{Request, Response, Fang, FangProc}; use std::sync::OnceLock; - impl Fang for Helmet { + impl Fang for Helmet { type Proc = HelmetProc; - fn chain(&self, inner: Inner) -> Self::Proc { - static SET_HEADERS: OnceLock>; + fn chain(&self, inner: I) -> Self::Proc { + static SET_HEADERS: OnceLock> = OnceLock::new(); - /* clone only once */ - let set_headers = SET_HEADERS.get_or_init({ + let set_headers = SET_HEADERS.get_or_init(|| { + /* clone only once */ + let helmet = self.clone(); - || { - - } + Box::new(move |res: &mut Response| {helmet.apply(res)}) }); HelmetProc { inner, set_headers } } } - struct HelmetProc { - set_headers: Box, + pub struct HelmetProc { + set_headers: &'static (dyn Fn(&mut Response) + Send + Sync), inner: I, } - impl FangProc for HelmetProc { - + impl FangProc for HelmetProc { + #[inline] + async fn bite<'f>(&'f self, req: &'f mut Request) -> Response { + let mut res = self.inner.bite(req).await; + (self.set_headers)(&mut res); + res + } } }; - -impl Helmet { - pub fn ContentSecurityPolicy(mut self, setter: impl FnOnce(field::ContentSecurityPolicy) -> field::ContentSecurityPolicy) -> Self { - self.ContentSecurityPolicy = Some(setter(ContentSecurityPolicy(String::new())).0); - self - } - pub fn ContentSecurityPolicyReportOnly(mut self, setter: impl FnOnce(field::ContentSecurityPolicy) -> field::ContentSecurityPolicy) -> Self { - self.ContentSecurityPolicyReportOnly = Some(setter(ContentSecurityPolicyReportOnly(String::new())).0); - self - } - pub fn CrossOriginEmbedderPolicy_require_corp(mut self) -> Self { - self.CrossOriginEmbedderPolicy_require_corp = true; - self - } -} - -mod field { - struct ContentSecurityPolicy(String); - impl ContentSecurityPolicy { - - } - - -} diff --git a/ohkami/src/response/headers.rs b/ohkami/src/response/headers.rs index c8cfa7ff8..21ff1cd4d 100644 --- a/ohkami/src/response/headers.rs +++ b/ohkami/src/response/headers.rs @@ -188,7 +188,7 @@ macro_rules! Header { } } }; -} Header! {45; +} Header! {47; AcceptRanges: b"Accept-Ranges", AccessControlAllowCredentials: b"Access-Control-Allow-Credentials", AccessControlAllowHeaders: b"Access-Control-Allow-Headers", From 0d7385919dcddbd0f61952330dc266a4a3da11bb Mon Sep 17 00:00:00 2001 From: kanarus Date: Thu, 13 Feb 2025 22:44:29 +0900 Subject: [PATCH 06/17] is a struct, not a module --- ohkami/src/fang/builtin.rs | 2 +- ohkami/src/fang/builtin/helmet.rs | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ohkami/src/fang/builtin.rs b/ohkami/src/fang/builtin.rs index 1787f260a..d15e74a8c 100644 --- a/ohkami/src/fang/builtin.rs +++ b/ohkami/src/fang/builtin.rs @@ -10,7 +10,7 @@ pub use jwt::{JWT, JWTToken}; mod context; pub use context::Context; -mod helmet; +pub mod helmet; pub use helmet::Helmet; #[cfg(feature="__rt_native__")] diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index 21588dbc0..cecf8a36f 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -145,13 +145,16 @@ const _: () = { /// /// ``` /// use ohkami::prelude::*; -/// use ohkami::fang::{Helmet, Sandbox::{allow_forms, allow_same_origin}}; +/// use ohkami::fang::helmet::{Helmet, CSP, Sandbox::{allow_forms, allow_same_origin}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( /// Helmet { -/// sandbox: allow_forms | allow_same_origin, +/// ContentSecurityPolicy: Some(CSP { +/// sandbox: allow_forms | allow_same_origin, +/// ..Default::default() +/// }), /// ..Default::default() /// }, /// "/hello".GET(|| async {"Hello, helmet!"}) @@ -215,13 +218,16 @@ const _: () = { /// /// ``` /// use ohkami::prelude::*; -/// use ohkami::fang::{Helmet, SouceList::{self_origin, data}}; +/// use ohkami::fang::helmet::{Helmet, CSP, SourceList::{self_origin, data}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( /// Helmet { -/// script_src: self_origin | data, +/// ContentSecurityPolicy: Some(CSP { +/// script_src: self_origin | data, +/// ..Default::default() +/// }), /// ..Default::default() /// }, /// "/hello".GET(|| async {"Hello, helmet!"}) @@ -351,9 +357,8 @@ const _: () = { static SET_HEADERS: OnceLock> = OnceLock::new(); let set_headers = SET_HEADERS.get_or_init(|| { - /* clone only once */ + /* clone **only once** */ let helmet = self.clone(); - Box::new(move |res: &mut Response| {helmet.apply(res)}) }); From 206a53de41f2f824b731024b01d47b2e580bf95a Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 01:28:42 +0900 Subject: [PATCH 07/17] improve interface --- ohkami/src/fang/builtin/helmet.rs | 247 +++++++++++++++++++----------- 1 file changed, 154 insertions(+), 93 deletions(-) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index cecf8a36f..9389cc434 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -1,16 +1,30 @@ /// based on , /// with removing non-standard or deprecated headers +/// +/// ## Example +/// +/// ```no_run +/// use ohkami::prelude::*; +/// use ohkami::fang::Helmet; +/// +/// #[tokio::main] +/// async fn main() { +/// Ohkami::new((Helmet::default(), +/// "/hello".GET(|| async {"Hello, Helmet!"}), +/// )).howl("localhost:4040").await +/// } +/// ``` #[derive(Clone)] #[allow(non_snake_case)] pub struct Helmet { - pub ContentSecurityPolicy: Option, - pub ContentSecurityPolicyReportOnly: Option, - pub CrossOriginEmbedderPolicy: &'static str, - pub CrossOriginResourcePolicy: &'static str, - pub ReferrerPolicy: &'static str, - pub StrictTransportSecurity: &'static str, - pub XContentTypeOptions: &'static str, - pub XFrameOptions: &'static str, + ContentSecurityPolicy: Option, + ContentSecurityPolicyReportOnly: Option, + CrossOriginEmbedderPolicy: &'static str, + CrossOriginResourcePolicy: &'static str, + ReferrerPolicy: &'static str, + StrictTransportSecurity: &'static str, + XContentTypeOptions: &'static str, + XFrameOptions: &'static str, } const _: () = { impl Default for Helmet { @@ -28,6 +42,42 @@ const _: () = { } } + #[allow(non_snake_case)] + impl Helmet { + /// default: no setting + pub fn ContentSecurityPolicy(mut self, csp: CSP) -> Self { + self.ContentSecurityPolicy = Some(csp); self + } + /// default: no setting + pub fn ContentSecurityPolicyReportOnly(mut self, csp: CSP) -> Self { + self.ContentSecurityPolicyReportOnly = Some(csp); self + } + /// default: `"require-corp"` + pub fn CrossOriginEmbedderPolicy(mut self, CrossOriginEmbedderPolicy: &'static str) -> Self { + self.CrossOriginEmbedderPolicy = CrossOriginEmbedderPolicy; self + } + /// default: `"same-origin"` + pub fn CrossOriginResourcePolicy(mut self, CrossOriginResourcePolicy: &'static str) -> Self { + self.CrossOriginResourcePolicy = CrossOriginResourcePolicy; self + } + /// default: `"no-referrer"` + pub fn ReferrerPolicy(mut self, ReferrerPolicy: &'static str) -> Self { + self.ReferrerPolicy = ReferrerPolicy; self + } + /// default: `"max-age=15552000; includeSubDomains"` + pub fn StrictTransportSecurity(mut self, StrictTransportSecurity: &'static str) -> Self { + self.StrictTransportSecurity = StrictTransportSecurity; self + } + /// default: `"nosniff"` + pub fn XContentTypeOptions(mut self, XContentTypeOptions: &'static str) -> Self { + self.XContentTypeOptions = XContentTypeOptions; self + } + /// default: `"SAMEORIGIN"` + pub fn XFrameOptions(mut self, XFrameOptions: &'static str) -> Self { + self.XFrameOptions = XFrameOptions; self + } + } + impl Helmet { pub(self) fn apply(&self, res: &mut crate::Response) { let mut h = res.headers.set(); @@ -60,28 +110,46 @@ const _: () = { }; /// based on +/// +/// ## Example +/// +/// ```no_run +/// use ohkami::prelude::*; +/// use ohkami::fang::helmet::{Helmet, CSP, sandbox::{allow_forms, allow_same_origin}}; +/// +/// #[tokio::main] +/// async fn main() { +/// Ohkami::new(( +/// Helmet::default() +/// .ContentSecurityPolicy(CSP { +/// sandbox: allow_forms | allow_same_origin, +/// ..Default::default() +/// }), +/// )).howl("localhost:4040").await +/// } +/// ``` #[derive(Clone, Default)] pub struct CSP { - pub default_src: SourceList, - pub script_src: SourceList, - pub style_src: SourceList, - pub img_src: SourceList, - pub connect_src: SourceList, - pub font_src: SourceList, - pub object_src: SourceList, - pub media_src: SourceList, - pub frame_src: SourceList, - pub sandbox: Sandbox, + pub default_src: source::SourceList, + pub script_src: source::SourceList, + pub style_src: source::SourceList, + pub img_src: source::SourceList, + pub connect_src: source::SourceList, + pub font_src: source::SourceList, + pub object_src: source::SourceList, + pub media_src: source::SourceList, + pub frame_src: source::SourceList, + pub sandbox: sandbox::Sandbox, pub report_uri: &'static str, - pub child_src: SourceList, + pub child_src: source::SourceList, pub form_action: &'static str, pub frame_ancestors: &'static str, pub plugin_types: &'static str, pub base_uri: &'static str, pub report_to: &'static str, - pub worker_src: SourceList, - pub manifest_src: SourceList, - pub prefetch_src: SourceList, + pub worker_src: source::SourceList, + pub manifest_src: source::SourceList, + pub prefetch_src: source::SourceList, pub navifate_to: &'static str, pub require_trusted_types_for: &'static str, pub trusted_types: &'static str, @@ -143,40 +211,36 @@ const _: () = { /// ## Example /// -/// ``` +/// ```no_run /// use ohkami::prelude::*; -/// use ohkami::fang::helmet::{Helmet, CSP, Sandbox::{allow_forms, allow_same_origin}}; +/// use ohkami::fang::helmet::{Helmet, CSP, sandbox::{allow_forms, allow_same_origin}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( -/// Helmet { -/// ContentSecurityPolicy: Some(CSP { +/// Helmet::default() +/// .ContentSecurityPolicy(CSP { /// sandbox: allow_forms | allow_same_origin, /// ..Default::default() /// }), -/// ..Default::default() -/// }, -/// "/hello".GET(|| async {"Hello, helmet!"}) -/// )).howl("localhost:3000").await +/// )).howl("localhost:4040").await /// } /// ``` -#[derive(Clone)] -pub struct Sandbox(u16); -const _: () = { - #[allow(non_upper_case_globals)] - impl Sandbox { - pub const allow_forms: Self = Self(0b0000000001u16); - pub const allow_same_origin: Self = Self(0b0000000010u16); - pub const allow_scripts: Self = Self(0b0000000100u16); - pub const allow_popups: Self = Self(0b0000001000u16); - pub const allow_modals: Self = Self(0b0000010000u16); - pub const allow_orientation_lock: Self = Self(0b0000100000u16); - pub const allow_pointer_lock: Self = Self(0b0001000000u16); - pub const allow_presentation: Self = Self(0b0010000000u16); - pub const allow_popups_to_escape_sandbox: Self = Self(0b0100000000u16); - pub const allow_top_navigation: Self = Self(0b1000000000u16); - } +#[allow(non_upper_case_globals)] +pub mod sandbox { + #[derive(Clone)] + pub struct Sandbox(u16); + + pub const allow_forms: Sandbox = Sandbox(0b0000000001u16); + pub const allow_same_origin: Sandbox = Sandbox(0b0000000010u16); + pub const allow_scripts: Sandbox = Sandbox(0b0000000100u16); + pub const allow_popups: Sandbox = Sandbox(0b0000001000u16); + pub const allow_modals: Sandbox = Sandbox(0b0000010000u16); + pub const allow_orientation_lock: Sandbox = Sandbox(0b0000100000u16); + pub const allow_pointer_lock: Sandbox = Sandbox(0b0001000000u16); + pub const allow_presentation: Sandbox = Sandbox(0b0010000000u16); + pub const allow_popups_to_escape_sandbox: Sandbox = Sandbox(0b0100000000u16); + pub const allow_top_navigation: Sandbox = Sandbox(0b1000000000u16); impl std::ops::BitOr for Sandbox { type Output = Self; @@ -193,53 +257,53 @@ const _: () = { } impl Sandbox { - pub(self) const fn is_empty(&self) -> bool { + pub(super) const fn is_empty(&self) -> bool { self.0 == 0b0000000000u16 } - pub(self) fn build(&self) -> String { + pub(super) fn build(&self) -> String { let mut result = String::new(); - if self.0 & Self::allow_forms.0 != 0 {result.push_str(" allow-forms");} - if self.0 & Self::allow_same_origin.0 != 0 {result.push_str(" allow-same-origin");} - if self.0 & Self::allow_scripts.0 != 0 {result.push_str(" allow-scripts");} - if self.0 & Self::allow_popups.0 != 0 {result.push_str(" allow-popups");} - if self.0 & Self::allow_modals.0 != 0 {result.push_str(" allow-modals");} - if self.0 & Self::allow_orientation_lock.0 != 0 {result.push_str(" allow-orientation-lock");} - if self.0 & Self::allow_pointer_lock.0 != 0 {result.push_str(" allow-pointer-lock");} - if self.0 & Self::allow_presentation.0 != 0 {result.push_str(" allow-presentation");} - if self.0 & Self::allow_popups_to_escape_sandbox.0 != 0 {result.push_str(" allow-popups-to-escape-sandbox");} - if self.0 & Self::allow_top_navigation.0 != 0 {result.push_str(" allow-top-navigation");} + if self.0 & allow_forms.0 != 0 {result.push_str(" allow-forms");} + if self.0 & allow_same_origin.0 != 0 {result.push_str(" allow-same-origin");} + if self.0 & allow_scripts.0 != 0 {result.push_str(" allow-scripts");} + if self.0 & allow_popups.0 != 0 {result.push_str(" allow-popups");} + if self.0 & allow_modals.0 != 0 {result.push_str(" allow-modals");} + if self.0 & allow_orientation_lock.0 != 0 {result.push_str(" allow-orientation-lock");} + if self.0 & allow_pointer_lock.0 != 0 {result.push_str(" allow-pointer-lock");} + if self.0 & allow_presentation.0 != 0 {result.push_str(" allow-presentation");} + if self.0 & allow_popups_to_escape_sandbox.0 != 0 {result.push_str(" allow-popups-to-escape-sandbox");} + if self.0 & allow_top_navigation.0 != 0 {result.push_str(" allow-top-navigation");} result } } -}; +} /// ## Example /// -/// ``` +/// ```no_run /// use ohkami::prelude::*; -/// use ohkami::fang::helmet::{Helmet, CSP, SourceList::{self_origin, data}}; +/// use ohkami::fang::helmet::{Helmet, CSP, source::{self_origin, data}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( -/// Helmet { -/// ContentSecurityPolicy: Some(CSP { +/// Helmet::default() +/// .ContentSecurityPolicy(CSP { /// script_src: self_origin | data, /// ..Default::default() /// }), -/// ..Default::default() -/// }, /// "/hello".GET(|| async {"Hello, helmet!"}) /// )).howl("localhost:3000").await /// } /// ``` -#[derive(Clone, Default)] -pub struct SourceList { - this: std::borrow::Cow<'static, str>, - list: Vec>, -} -const _: () = { +#[allow(non_upper_case_globals)] +pub mod source { + #[derive(Clone, Default)] + pub struct SourceList { + this: std::borrow::Cow<'static, str>, + list: Vec>, + } + #[derive(Clone)] #[allow(non_camel_case_types)] pub enum Source { @@ -259,7 +323,7 @@ const _: () = { nonce(String), } impl Source { - pub(self) const fn build_const(&self) -> std::borrow::Cow<'static, str> { + const fn build_const(&self) -> std::borrow::Cow<'static, str> { match self { Self::any => std::borrow::Cow::Borrowed("*"), Self::data => std::borrow::Cow::Borrowed("data:"), @@ -277,7 +341,7 @@ const _: () = { Self::nonce(_) => unreachable!(), } } - pub(self) fn build_hash(&self) -> std::borrow::Cow<'static, str> { + fn build_hash(&self) -> std::borrow::Cow<'static, str> { match self { Self::any => unreachable!(), Self::data => unreachable!(), @@ -301,23 +365,20 @@ const _: () = { (const $src:expr) => {SourceList { this: $src.build_const(), list: Vec::new() }}; (hash $src:expr) => {SourceList { this: $src.build_hash(), list: Vec::new() }}; } - #[allow(non_upper_case_globals)] - impl SourceList { - pub const any: Self = this!(const Source::any); - pub const data: Self = this!(const Source::data); - pub const https: Self = this!(const Source::https); - pub const none: Self = this!(const Source::none); - pub const self_origin: Self = this!(const Source::self_origin); - pub const strict_dynamic: Self = this!(const Source::strict_dynamic); - pub const unsafe_inline: Self = this!(const Source::unsafe_inline); - pub const unsafe_eval: Self = this!(const Source::unsafe_eval); - pub const unsafe_hashes: Self = this!(const Source::unsafe_hashes); - pub fn domain(domain: &'static str) -> Self {this!(const Source::domain(domain))} - pub fn sha256(sha256: String) -> Self {this!(hash Source::sha256(sha256))} - pub fn sha384(sha384: String) -> Self {this!(hash Source::sha384(sha384))} - pub fn sha512(sha512: String) -> Self {this!(hash Source::sha512(sha512))} - pub fn nonce (nonce: String) -> Self {this!(hash Source::nonce(nonce))} - } + pub const any: SourceList = this!(const Source::any); + pub const data: SourceList = this!(const Source::data); + pub const https: SourceList = this!(const Source::https); + pub const none: SourceList = this!(const Source::none); + pub const self_origin: SourceList = this!(const Source::self_origin); + pub const strict_dynamic: SourceList = this!(const Source::strict_dynamic); + pub const unsafe_inline: SourceList = this!(const Source::unsafe_inline); + pub const unsafe_eval: SourceList = this!(const Source::unsafe_eval); + pub const unsafe_hashes: SourceList = this!(const Source::unsafe_hashes); + pub fn domain(domain: &'static str) -> SourceList {this!(const Source::domain(domain))} + pub fn sha256(sha256: String) -> SourceList {this!(hash Source::sha256(sha256))} + pub fn sha384(sha384: String) -> SourceList {this!(hash Source::sha384(sha384))} + pub fn sha512(sha512: String) -> SourceList {this!(hash Source::sha512(sha512))} + pub fn nonce(nonce: String) -> SourceList {this!(hash Source::nonce(nonce))} impl std::ops::BitOr for SourceList { type Output = Self; @@ -330,11 +391,11 @@ const _: () = { } impl SourceList { - pub(self) fn is_empty(&self) -> bool { + pub(super) fn is_empty(&self) -> bool { self.this.is_empty() } - pub(self) fn build(&self) -> String { + pub(super) fn build(&self) -> String { let mut result = String::from(&*self.this); if !self.list.is_empty() { for s in &self.list { @@ -345,7 +406,7 @@ const _: () = { result } } -}; +} const _: () = { use crate::{Request, Response, Fang, FangProc}; From d449584746b36ae8a38ced74a9bc98ad31cd13ec Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 01:52:05 +0900 Subject: [PATCH 08/17] working skelton --- ohkami/src/fang/builtin/helmet.rs | 84 +++++++++++++++++++++++++++++++ ohkami/src/response/mod.rs | 35 ++++++------- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index 9389cc434..b038107af 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -441,3 +441,87 @@ const _: () = { } } }; + + + + +#[cfg(test)] +mod test { + use super::*; + use crate::prelude::*; + use crate::testing::*; + use std::collections::HashSet; + + #[test] + fn test_helmet_set_headers() { + let t = Ohkami::new(( + Helmet::default(), + "/hello".GET(|| async {"Hello, helmet!"}), + )).test(); + + crate::__rt__::testing::block_on(async { + /* matched case */ + { + let req = TestRequest::GET("/hello"); + let res = t.oneshot(req).await; + assert_eq!(res.status().code(), 200); + assert_eq!(res.text().unwrap(), "Hello, helmet!"); + assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + ("Cross-Origin-Embedder-Policy", "require-corp"), + ("Cross-Origin-Resource-Policy", "same-origin"), + ("Referrer-Policy", "no-referrer"), + ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), + ("X-Content-Type-Options", "nosniff"), + ("X-Frame-Options", "SAMEORIGIN"), + + ("Content-Type", "text/plain; charset=UTF-8"), + ("Content-Length", "14"), + ])); + } + + /* any Not Found cases */ + { + let req = TestRequest::GET("/"); + let res = t.oneshot(req).await; + assert_eq!(res.status().code(), 404); + assert_eq!(res.text(), None); + assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + ("Cross-Origin-Embedder-Policy", "require-corp"), + ("Cross-Origin-Resource-Policy", "same-origin"), + ("Referrer-Policy", "no-referrer"), + ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), + ("X-Content-Type-Options", "nosniff"), + ("X-Frame-Options", "SAMEORIGIN"), + ])); + } + { + let req = TestRequest::POST("/hello"); + let res = t.oneshot(req).await; + assert_eq!(res.status().code(), 404); + assert_eq!(res.text(), None); + assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + ("Cross-Origin-Embedder-Policy", "require-corp"), + ("Cross-Origin-Resource-Policy", "same-origin"), + ("Referrer-Policy", "no-referrer"), + ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), + ("X-Content-Type-Options", "nosniff"), + ("X-Frame-Options", "SAMEORIGIN"), + ])); + } + { + let req = TestRequest::DELETE("/"); + let res = t.oneshot(req).await; + assert_eq!(res.status().code(), 404); + assert_eq!(res.text(), None); + assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + ("Cross-Origin-Embedder-Policy", "require-corp"), + ("Cross-Origin-Resource-Policy", "same-origin"), + ("Referrer-Policy", "no-referrer"), + ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), + ("X-Content-Type-Options", "nosniff"), + ("X-Frame-Options", "SAMEORIGIN"), + ])); + } + }); + } +} diff --git a/ohkami/src/response/mod.rs b/ohkami/src/response/mod.rs index 2988db640..84d4acac0 100644 --- a/ohkami/src/response/mod.rs +++ b/ohkami/src/response/mod.rs @@ -16,7 +16,7 @@ pub use into_response::{IntoResponse, IntoBody}; #[cfg(test)] mod _test_headers; use std::borrow::Cow; -use ohkami_lib::{CowSlice, Slice}; +use ohkami_lib::{CowSlice, Slice, num}; #[cfg(feature="__rt_native__")] use crate::__rt__::AsyncWrite; @@ -177,6 +177,7 @@ impl Response { self } + #[inline] pub fn set_payload(&mut self, content_type: &'static str, content: impl Into>, @@ -184,9 +185,10 @@ impl Response { let content = content.into(); self.headers.set() .ContentType(content_type) - .ContentLength(content.len().to_string()); + .ContentLength(num::itoa(content.len())); self.content = Content::Payload(content.into()); } + #[inline] pub fn with_payload(mut self, content_type: &'static str, content: impl Into>, @@ -198,33 +200,35 @@ impl Response { self.content.as_bytes() } - #[inline] pub fn set_text>>(&mut self, text: Text) { - let body = text.into(); + #[inline] + pub fn set_text>>(&mut self, text: Text) { + let body: Cow<'static, str> = text.into(); self.headers.set() - .ContentType("text/plain; charset=UTF-8"); + .ContentType("text/plain; charset=UTF-8") + .ContentLength(num::itoa(body.len())); self.content = Content::Payload(match body { Cow::Borrowed(str) => CowSlice::Ref(Slice::from_bytes(str.as_bytes())), Cow::Owned(string) => CowSlice::Own(string.into_bytes().into()), }); } - #[inline(always)] pub fn with_text>>(mut self, text: Text) -> Self { + #[inline(always)] + pub fn with_text>>(mut self, text: Text) -> Self { self.set_text(text); self } - #[inline(always)] pub fn set_html>>(&mut self, html: HTML) { - let body = html.into(); + let body: Cow<'static, str> = html.into(); self.headers.set() - .ContentType("text/html; charset=UTF-8"); + .ContentType("text/html; charset=UTF-8") + .ContentLength(num::itoa(body.len())); self.content = Content::Payload(match body { Cow::Borrowed(str) => CowSlice::Ref(Slice::from_bytes(str.as_bytes())), Cow::Owned(string) => CowSlice::Own(string.into_bytes().into()), }); } - #[inline(always)] pub fn with_html>>(mut self, html: HTML) -> Self { self.set_html(html); self @@ -233,9 +237,9 @@ impl Response { #[inline(always)] pub fn set_json(&mut self, json: JSON) { let body = ::serde_json::to_vec(&json).unwrap(); - self.headers.set() - .ContentType("application/json"); + .ContentType("application/json") + .ContentLength(num::itoa(body.len())); self.content = Content::Payload(body.into()); } #[inline(always)] @@ -244,7 +248,7 @@ impl Response { self } - /// SAFETY: Argument `json_lit` is **valid JSON** + /// SAFETY: argument `json_lit` must be **valid JSON** pub unsafe fn set_json_lit>>(&mut self, json_lit: JSONLiteral) { let body = match json_lit.into() { Cow::Borrowed(str) => Cow::Borrowed(str.as_bytes()), @@ -255,7 +259,7 @@ impl Response { .ContentType("application/json"); self.content = Content::Payload(body.into()); } - /// SAFETY: Argument `json_lit` is **valid JSON** + /// SAFETY: argument `json_lit` must be **valid JSON** pub unsafe fn with_json_lit>>(mut self, json_lit: JSONLiteral) -> Self { self.set_json_lit(json_lit); self @@ -264,7 +268,6 @@ impl Response { #[cfg(feature="sse")] impl Response { - #[inline] pub fn with_stream( mut self, stream: impl Stream + Unpin + Send + 'static @@ -273,7 +276,6 @@ impl Response { self } - #[inline] pub fn set_stream( &mut self, stream: impl Stream + Unpin + Send + 'static @@ -281,7 +283,6 @@ impl Response { self.set_stream_raw(Box::pin(stream.map(sse::Data::encode))); } - #[inline] pub fn set_stream_raw( &mut self, stream: std::pin::Pin + Send>> From cff174763ec69dd33a139e7cec217f98ca46f653 Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 02:47:50 +0900 Subject: [PATCH 09/17] fix format & add CSP test --- ohkami/src/fang/builtin/helmet.rs | 219 +++++++++++++++++------------- 1 file changed, 124 insertions(+), 95 deletions(-) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index b038107af..d60556240 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -15,8 +15,9 @@ /// } /// ``` #[derive(Clone)] +pub struct Helmet(std::sync::Arc); #[allow(non_snake_case)] -pub struct Helmet { +struct HelmetFields { ContentSecurityPolicy: Option, ContentSecurityPolicyReportOnly: Option, CrossOriginEmbedderPolicy: &'static str, @@ -29,7 +30,7 @@ pub struct Helmet { const _: () = { impl Default for Helmet { fn default() -> Self { - Helmet { + Self(std::sync::Arc::new(HelmetFields { ContentSecurityPolicy: None, ContentSecurityPolicyReportOnly: None, CrossOriginEmbedderPolicy: "require-corp", @@ -38,75 +39,97 @@ const _: () = { StrictTransportSecurity: "max-age=15552000; includeSubDomains", XContentTypeOptions: "nosniff", XFrameOptions: "SAMEORIGIN", - } + })) } } + fn inner_mut(h: &mut Helmet) -> &mut HelmetFields { + std::sync::Arc::get_mut(&mut h.0).expect("Helmet unexpectedly already cloned by someone before Fang::chain") + } + #[allow(non_snake_case)] impl Helmet { /// default: no setting pub fn ContentSecurityPolicy(mut self, csp: CSP) -> Self { - self.ContentSecurityPolicy = Some(csp); self + inner_mut(&mut self).ContentSecurityPolicy = Some(csp); self } /// default: no setting pub fn ContentSecurityPolicyReportOnly(mut self, csp: CSP) -> Self { - self.ContentSecurityPolicyReportOnly = Some(csp); self + inner_mut(&mut self).ContentSecurityPolicyReportOnly = Some(csp); self } /// default: `"require-corp"` + /// + /// set to `""` ( empty string ) for disabling the header pub fn CrossOriginEmbedderPolicy(mut self, CrossOriginEmbedderPolicy: &'static str) -> Self { - self.CrossOriginEmbedderPolicy = CrossOriginEmbedderPolicy; self + inner_mut(&mut self).CrossOriginEmbedderPolicy = CrossOriginEmbedderPolicy; self } /// default: `"same-origin"` + /// + /// set to `""` ( empty string ) for disabling the header pub fn CrossOriginResourcePolicy(mut self, CrossOriginResourcePolicy: &'static str) -> Self { - self.CrossOriginResourcePolicy = CrossOriginResourcePolicy; self + inner_mut(&mut self).CrossOriginResourcePolicy = CrossOriginResourcePolicy; self } /// default: `"no-referrer"` + /// + /// set to `""` ( empty string ) for disabling the header pub fn ReferrerPolicy(mut self, ReferrerPolicy: &'static str) -> Self { - self.ReferrerPolicy = ReferrerPolicy; self + inner_mut(&mut self).ReferrerPolicy = ReferrerPolicy; self } /// default: `"max-age=15552000; includeSubDomains"` + /// + /// set to `""` ( empty string ) for disabling the header pub fn StrictTransportSecurity(mut self, StrictTransportSecurity: &'static str) -> Self { - self.StrictTransportSecurity = StrictTransportSecurity; self + inner_mut(&mut self).StrictTransportSecurity = StrictTransportSecurity; self } /// default: `"nosniff"` + /// + /// set to `""` ( empty string ) for disabling the header pub fn XContentTypeOptions(mut self, XContentTypeOptions: &'static str) -> Self { - self.XContentTypeOptions = XContentTypeOptions; self + inner_mut(&mut self).XContentTypeOptions = XContentTypeOptions; self } /// default: `"SAMEORIGIN"` + /// + /// set to `""` ( empty string ) for disabling the header pub fn XFrameOptions(mut self, XFrameOptions: &'static str) -> Self { - self.XFrameOptions = XFrameOptions; self + inner_mut(&mut self).XFrameOptions = XFrameOptions; self } } impl Helmet { - pub(self) fn apply(&self, res: &mut crate::Response) { + fn apply(&self, res: &mut crate::Response) { let mut h = res.headers.set(); - if let Some(csp) = &self.ContentSecurityPolicy { + if let Some(csp) = &self.0.ContentSecurityPolicy { h = h.ContentSecurityPolicy(csp.build()); } - if let Some(csp) = &self.ContentSecurityPolicyReportOnly { + if let Some(csp) = &self.0.ContentSecurityPolicyReportOnly { h = h.ContentSecurityPolicyReportOnly(csp.build()); } - if !self.CrossOriginEmbedderPolicy.is_empty() { - h = h.CrossOriginEmbedderPolicy(self.CrossOriginEmbedderPolicy); + if !self.0.CrossOriginEmbedderPolicy.is_empty() { + h = h.CrossOriginEmbedderPolicy(self.0.CrossOriginEmbedderPolicy); } - if !self.CrossOriginResourcePolicy.is_empty() { - h = h.CrossOriginResourcePolicy(self.CrossOriginResourcePolicy); + if !self.0.CrossOriginResourcePolicy.is_empty() { + h = h.CrossOriginResourcePolicy(self.0.CrossOriginResourcePolicy); } - if !self.ReferrerPolicy.is_empty() { - h = h.ReferrerPolicy(self.ReferrerPolicy); + if !self.0.ReferrerPolicy.is_empty() { + h = h.ReferrerPolicy(self.0.ReferrerPolicy); } - if !self.StrictTransportSecurity.is_empty() { - h = h.StrictTransportSecurity(self.StrictTransportSecurity); + if !self.0.StrictTransportSecurity.is_empty() { + h = h.StrictTransportSecurity(self.0.StrictTransportSecurity); } - if !self.XContentTypeOptions.is_empty() { - h = h.XContentTypeOptions(self.XContentTypeOptions); + if !self.0.XContentTypeOptions.is_empty() { + h = h.XContentTypeOptions(self.0.XContentTypeOptions); } - if !self.XFrameOptions.is_empty() { - h.XFrameOptions(self.XFrameOptions); + if !self.0.XFrameOptions.is_empty() { + h.XFrameOptions(self.0.XFrameOptions); } } } + + impl crate::fang::FangAction for Helmet { + async fn back<'a>(&'a self, res: &'a mut crate::Response) { + self.apply(res); + } + } }; /// based on @@ -128,28 +151,28 @@ const _: () = { /// )).howl("localhost:4040").await /// } /// ``` -#[derive(Clone, Default)] +#[derive(Default)] pub struct CSP { - pub default_src: source::SourceList, - pub script_src: source::SourceList, - pub style_src: source::SourceList, - pub img_src: source::SourceList, - pub connect_src: source::SourceList, - pub font_src: source::SourceList, - pub object_src: source::SourceList, - pub media_src: source::SourceList, - pub frame_src: source::SourceList, + pub default_src: src::SourceList, + pub script_src: src::SourceList, + pub style_src: src::SourceList, + pub img_src: src::SourceList, + pub connect_src: src::SourceList, + pub font_src: src::SourceList, + pub object_src: src::SourceList, + pub media_src: src::SourceList, + pub frame_src: src::SourceList, pub sandbox: sandbox::Sandbox, pub report_uri: &'static str, - pub child_src: source::SourceList, + pub child_src: src::SourceList, pub form_action: &'static str, pub frame_ancestors: &'static str, pub plugin_types: &'static str, pub base_uri: &'static str, pub report_to: &'static str, - pub worker_src: source::SourceList, - pub manifest_src: source::SourceList, - pub prefetch_src: source::SourceList, + pub worker_src: src::SourceList, + pub manifest_src: src::SourceList, + pub prefetch_src: src::SourceList, pub navifate_to: &'static str, pub require_trusted_types_for: &'static str, pub trusted_types: &'static str, @@ -166,14 +189,14 @@ const _: () = { if !(self.$field.is_empty()) { result.push_str(concat!($policy, " ")); result.push_str(&*self.$field.build()); - result.push(';'); + result.push_str("; "); } }; ($field:ident as $policy:literal) => { if !(self.$field.is_empty()) { result.push_str(concat!($policy, " ")); result.push_str(self.$field); - result.push(';'); + result.push_str("; "); } }; } @@ -204,6 +227,7 @@ const _: () = { append!(upgrade_insecure_requests as "upgrade-insecure-requests"); append!(block_all_mixed_content as "block-all-mixed-content"); + if result.ends_with(' ') {let _ = result.pop();} result } } @@ -228,7 +252,6 @@ const _: () = { /// ``` #[allow(non_upper_case_globals)] pub mod sandbox { - #[derive(Clone)] pub struct Sandbox(u16); pub const allow_forms: Sandbox = Sandbox(0b0000000001u16); @@ -263,16 +286,17 @@ pub mod sandbox { pub(super) fn build(&self) -> String { let mut result = String::new(); - if self.0 & allow_forms.0 != 0 {result.push_str(" allow-forms");} - if self.0 & allow_same_origin.0 != 0 {result.push_str(" allow-same-origin");} - if self.0 & allow_scripts.0 != 0 {result.push_str(" allow-scripts");} - if self.0 & allow_popups.0 != 0 {result.push_str(" allow-popups");} - if self.0 & allow_modals.0 != 0 {result.push_str(" allow-modals");} - if self.0 & allow_orientation_lock.0 != 0 {result.push_str(" allow-orientation-lock");} - if self.0 & allow_pointer_lock.0 != 0 {result.push_str(" allow-pointer-lock");} - if self.0 & allow_presentation.0 != 0 {result.push_str(" allow-presentation");} - if self.0 & allow_popups_to_escape_sandbox.0 != 0 {result.push_str(" allow-popups-to-escape-sandbox");} - if self.0 & allow_top_navigation.0 != 0 {result.push_str(" allow-top-navigation");} + if self.0 & allow_forms.0 != 0 {result.push_str("allow-forms ");} + if self.0 & allow_same_origin.0 != 0 {result.push_str("allow-same-origin ");} + if self.0 & allow_scripts.0 != 0 {result.push_str("allow-scripts ");} + if self.0 & allow_popups.0 != 0 {result.push_str("allow-popups ");} + if self.0 & allow_modals.0 != 0 {result.push_str("allow-modals ");} + if self.0 & allow_orientation_lock.0 != 0 {result.push_str("allow-orientation-lock ");} + if self.0 & allow_pointer_lock.0 != 0 {result.push_str("allow-pointer-lock ");} + if self.0 & allow_presentation.0 != 0 {result.push_str("allow-presentation ");} + if self.0 & allow_popups_to_escape_sandbox.0 != 0 {result.push_str("allow-popups-to-escape-sandbox ");} + if self.0 & allow_top_navigation.0 != 0 {result.push_str("allow-top-navigation ");} + if result.ends_with(' ') {let _ = result.pop();} result } } @@ -282,7 +306,7 @@ pub mod sandbox { /// /// ```no_run /// use ohkami::prelude::*; -/// use ohkami::fang::helmet::{Helmet, CSP, source::{self_origin, data}}; +/// use ohkami::fang::helmet::{Helmet, CSP, src::{self_origin, data}}; /// /// #[tokio::main] /// async fn main() { @@ -297,14 +321,13 @@ pub mod sandbox { /// } /// ``` #[allow(non_upper_case_globals)] -pub mod source { - #[derive(Clone, Default)] +pub mod src { + #[derive(Default)] pub struct SourceList { this: std::borrow::Cow<'static, str>, list: Vec>, } - #[derive(Clone)] #[allow(non_camel_case_types)] pub enum Source { any, @@ -408,42 +431,6 @@ pub mod source { } } -const _: () = { - use crate::{Request, Response, Fang, FangProc}; - use std::sync::OnceLock; - - impl Fang for Helmet { - type Proc = HelmetProc; - fn chain(&self, inner: I) -> Self::Proc { - static SET_HEADERS: OnceLock> = OnceLock::new(); - - let set_headers = SET_HEADERS.get_or_init(|| { - /* clone **only once** */ - let helmet = self.clone(); - Box::new(move |res: &mut Response| {helmet.apply(res)}) - }); - - HelmetProc { inner, set_headers } - } - } - - pub struct HelmetProc { - set_headers: &'static (dyn Fn(&mut Response) + Send + Sync), - inner: I, - } - - impl FangProc for HelmetProc { - #[inline] - async fn bite<'f>(&'f self, req: &'f mut Request) -> Response { - let mut res = self.inner.bite(req).await; - (self.set_headers)(&mut res); - res - } - } -}; - - - #[cfg(test)] mod test { @@ -453,7 +440,7 @@ mod test { use std::collections::HashSet; #[test] - fn test_helmet_set_headers() { + fn helmet_set_headers() { let t = Ohkami::new(( Helmet::default(), "/hello".GET(|| async {"Hello, helmet!"}), @@ -473,7 +460,7 @@ mod test { ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), ("X-Content-Type-Options", "nosniff"), ("X-Frame-Options", "SAMEORIGIN"), - + ("Content-Type", "text/plain; charset=UTF-8"), ("Content-Length", "14"), ])); @@ -524,4 +511,46 @@ mod test { } }); } + + #[test] + fn helmet_csp() { + use src::{self_origin, https, domain}; + use sandbox::{allow_forms, allow_modals}; + + let t = Ohkami::new(( + Helmet::default() + .ContentSecurityPolicy(CSP { + default_src: self_origin | https | domain("*.example.com"), + sandbox: allow_forms | allow_modals, + report_uri: "https://my-report.uri", + ..Default::default() + }), + "/hello" + .GET(|| async {"Hello, helmet!"}), + )).test(); + + crate::__rt__::testing::block_on(async { + { + let req = TestRequest::GET("/hello"); + let res = t.oneshot(req).await; + assert_eq!(res.status().code(), 200); + assert_eq!(res.text().unwrap(), "Hello, helmet!"); + assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + /* defaults */ + ("Cross-Origin-Embedder-Policy", "require-corp"), + ("Cross-Origin-Resource-Policy", "same-origin"), + ("Referrer-Policy", "no-referrer"), + ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), + ("X-Content-Type-Options", "nosniff"), + ("X-Frame-Options", "SAMEORIGIN"), + + /* CSP */ + ("Content-Security-Policy", "default-src 'self' https: *.example.com; sandbox allow-forms allow-modals; report-uri https://my-report.uri;"), + + ("Content-Type", "text/plain; charset=UTF-8"), + ("Content-Length", "14"), + ])); + } + }); + } } From 4ba8651490b4ceb5e12a1e43dae2e2d07f4f5d7d Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 02:53:18 +0900 Subject: [PATCH 10/17] add doc for src::* consts --- ohkami/src/fang/builtin/helmet.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index d60556240..e4292ae60 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -388,19 +388,33 @@ pub mod src { (const $src:expr) => {SourceList { this: $src.build_const(), list: Vec::new() }}; (hash $src:expr) => {SourceList { this: $src.build_hash(), list: Vec::new() }}; } + /// `*` pub const any: SourceList = this!(const Source::any); + /// `data:` pub const data: SourceList = this!(const Source::data); + /// `https:` pub const https: SourceList = this!(const Source::https); + /// `'none'` pub const none: SourceList = this!(const Source::none); + /// `'self'` pub const self_origin: SourceList = this!(const Source::self_origin); + /// `'strict-dynamic'` pub const strict_dynamic: SourceList = this!(const Source::strict_dynamic); + /// `'unsafe-inline'` pub const unsafe_inline: SourceList = this!(const Source::unsafe_inline); + /// `'unsafe-eval'` pub const unsafe_eval: SourceList = this!(const Source::unsafe_eval); + /// `'unsafe-hashes'` pub const unsafe_hashes: SourceList = this!(const Source::unsafe_hashes); + /// like `domain.example.com`, `*.example.com`, `https://cdn.com` pub fn domain(domain: &'static str) -> SourceList {this!(const Source::domain(domain))} + /// `'sha256-{sha256}'` pub fn sha256(sha256: String) -> SourceList {this!(hash Source::sha256(sha256))} + /// `'sha384-{sha384}'` pub fn sha384(sha384: String) -> SourceList {this!(hash Source::sha384(sha384))} + /// `'sha512-{sha512}'` pub fn sha512(sha512: String) -> SourceList {this!(hash Source::sha512(sha512))} + /// `'nonce-{nonce}'` pub fn nonce(nonce: String) -> SourceList {this!(hash Source::nonce(nonce))} impl std::ops::BitOr for SourceList { From addb84e62a19570f482c74143b7dab8e8c51a486 Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 20:20:55 +0900 Subject: [PATCH 11/17] hide clone-ability --- ohkami/src/fang/builtin/helmet.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index e4292ae60..52209abf1 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -14,7 +14,6 @@ /// )).howl("localhost:4040").await /// } /// ``` -#[derive(Clone)] pub struct Helmet(std::sync::Arc); #[allow(non_snake_case)] struct HelmetFields { @@ -125,9 +124,26 @@ const _: () = { } } - impl crate::fang::FangAction for Helmet { - async fn back<'a>(&'a self, res: &'a mut crate::Response) { - self.apply(res); + use crate::{Request, Response, Fang, FangProc}; + + impl Fang for Helmet { + type Proc = HelmetProc; + fn chain(&self, inner: I) -> Self::Proc { + let helmet = Helmet(std::sync::Arc::clone(&self.0)); + HelmetProc { helmet, inner } + } + } + + pub struct HelmetProc { + helmet: Helmet, + inner: I, + } + + impl FangProc for HelmetProc { + async fn bite<'f>(&'f self, req: &'f mut Request) -> Response { + let mut res = self.inner.bite(req).await; + self.helmet.apply(&mut res); + res } } }; From e4307ac61c97a7035f9e2e828a318c3349e1b05c Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 20:22:23 +0900 Subject: [PATCH 12/17] add test for disabling header by setting `""` --- ohkami/src/fang/builtin/helmet.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/helmet.rs index 52209abf1..64d597fa8 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/helmet.rs @@ -583,4 +583,35 @@ mod test { } }); } + + #[test] + fn helmet_disable_header() { + let t = Ohkami::new(( + Helmet::default() + .CrossOriginEmbedderPolicy("") + .CrossOriginResourcePolicy(""), + "/hello" + .GET(|| async {"Hello, helmet!"}), + )).test(); + + crate::__rt__::testing::block_on(async { + { + let req = TestRequest::GET("/hello"); + let res = t.oneshot(req).await; + assert_eq!(res.status().code(), 200); + assert_eq!(res.text().unwrap(), "Hello, helmet!"); + assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + /* ("Cross-Origin-Embedder-Policy", "require-corp"), */ + /* ("Cross-Origin-Resource-Policy", "same-origin"), */ + ("Referrer-Policy", "no-referrer"), + ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), + ("X-Content-Type-Options", "nosniff"), + ("X-Frame-Options", "SAMEORIGIN"), + + ("Content-Type", "text/plain; charset=UTF-8"), + ("Content-Length", "14"), + ])); + } + }); + } } From 8b8e8dd26a9b0de38a93c226a1445f020db26d88 Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 20:37:50 +0900 Subject: [PATCH 13/17] rename to `Enamel` & extend docs --- README.md | 6 +- .../src/fang/builtin/{helmet.rs => enamel.rs} | 106 +++++++++++------- 2 files changed, 69 insertions(+), 43 deletions(-) rename ohkami/src/fang/builtin/{helmet.rs => enamel.rs} (90%) diff --git a/README.md b/README.md index a2c80b270..070df65e0 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ async fn main() { Ohkami's request handling system is called "**fang**s", and middlewares are implemented on this. -*builtin fang* : `CORS`, `JWT`, `BasicAuth`, `Timeout`, `Context` +*builtin fang* : `Context`, `CORS`, `JWT`, `BasicAuth`, `Timeout`, `Enamel` *( experimantal )* ```rust,no_run use ohkami::prelude::*; @@ -366,10 +366,10 @@ use ohkami::prelude::*; #[tokio::main] async fn main() { Ohkami::new(( - "/hello/:name" - .GET(hello), "/hello/:name/:n" .GET(hello_n), + "/hello/:name" + .GET(hello), "/search" .GET(search), )).howl("localhost:5000").await diff --git a/ohkami/src/fang/builtin/helmet.rs b/ohkami/src/fang/builtin/enamel.rs similarity index 90% rename from ohkami/src/fang/builtin/helmet.rs rename to ohkami/src/fang/builtin/enamel.rs index 64d597fa8..9863e1227 100644 --- a/ohkami/src/fang/builtin/helmet.rs +++ b/ohkami/src/fang/builtin/enamel.rs @@ -1,22 +1,42 @@ -/// based on , +/// # Builtin security headers fang +/// +/// Based on , /// with removing non-standard or deprecated headers /// +/// ## What it does +/// +/// By default, adds to response headers : +/// +/// - `Cross-Origin-Embedder-Policy` to `require-corp` +/// - `Cross-Origin-Resource-Policy` to `same-origin` +/// - `Referrer-Policy` to `no-referrer` +/// - `Strict-Transport-Security` to `max-age=15552000; includeSubDomains` +/// - `X-Content-Type-Options` to `nosniff` +/// - `XFrameOptions` to `SAMEORIGIN` +/// +/// Each of these defaults can be overrided by corresponded builder method. +/// +/// Additionally, `Content-Security-Policy` or `Content-Security-Policy-Report-Only` +/// can be set by the methods with `enamel::CSP`. +/// /// ## Example /// /// ```no_run /// use ohkami::prelude::*; -/// use ohkami::fang::Helmet; +/// use ohkami::fang::Enamel; /// /// #[tokio::main] /// async fn main() { -/// Ohkami::new((Helmet::default(), -/// "/hello".GET(|| async {"Hello, Helmet!"}), +/// Ohkami::new((Enamel::default(), +/// "/hello".GET(|| async {"Hello, Enamel!"}), /// )).howl("localhost:4040").await /// } /// ``` -pub struct Helmet(std::sync::Arc); +pub struct Enamel( + std::sync::Arc +); #[allow(non_snake_case)] -struct HelmetFields { +struct EnamelFields { ContentSecurityPolicy: Option, ContentSecurityPolicyReportOnly: Option, CrossOriginEmbedderPolicy: &'static str, @@ -27,9 +47,9 @@ struct HelmetFields { XFrameOptions: &'static str, } const _: () = { - impl Default for Helmet { + impl Default for Enamel { fn default() -> Self { - Self(std::sync::Arc::new(HelmetFields { + Self(std::sync::Arc::new(EnamelFields { ContentSecurityPolicy: None, ContentSecurityPolicyReportOnly: None, CrossOriginEmbedderPolicy: "require-corp", @@ -42,12 +62,12 @@ const _: () = { } } - fn inner_mut(h: &mut Helmet) -> &mut HelmetFields { - std::sync::Arc::get_mut(&mut h.0).expect("Helmet unexpectedly already cloned by someone before Fang::chain") + fn inner_mut(h: &mut Enamel) -> &mut EnamelFields { + std::sync::Arc::get_mut(&mut h.0).expect("Enamel unexpectedly already cloned by someone before Fang::chain") } #[allow(non_snake_case)] - impl Helmet { + impl Enamel { /// default: no setting pub fn ContentSecurityPolicy(mut self, csp: CSP) -> Self { inner_mut(&mut self).ContentSecurityPolicy = Some(csp); self @@ -94,7 +114,7 @@ const _: () = { } } - impl Helmet { + impl Enamel { fn apply(&self, res: &mut crate::Response) { let mut h = res.headers.set(); if let Some(csp) = &self.0.ContentSecurityPolicy { @@ -126,40 +146,42 @@ const _: () = { use crate::{Request, Response, Fang, FangProc}; - impl Fang for Helmet { - type Proc = HelmetProc; + impl Fang for Enamel { + type Proc = EnamelProc; fn chain(&self, inner: I) -> Self::Proc { - let helmet = Helmet(std::sync::Arc::clone(&self.0)); - HelmetProc { helmet, inner } + let enamel = Enamel(std::sync::Arc::clone(&self.0)); + EnamelProc { enamel, inner } } } - pub struct HelmetProc { - helmet: Helmet, + pub struct EnamelProc { + enamel: Enamel, inner: I, } - impl FangProc for HelmetProc { + impl FangProc for EnamelProc { async fn bite<'f>(&'f self, req: &'f mut Request) -> Response { let mut res = self.inner.bite(req).await; - self.helmet.apply(&mut res); + self.enamel.apply(&mut res); res } } }; -/// based on +/// # Typed `Content-Security-Policy` for `fang::Enamel` +/// +/// Based on /// /// ## Example /// /// ```no_run /// use ohkami::prelude::*; -/// use ohkami::fang::helmet::{Helmet, CSP, sandbox::{allow_forms, allow_same_origin}}; +/// use ohkami::fang::enamel::{Enamel, CSP, sandbox::{allow_forms, allow_same_origin}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( -/// Helmet::default() +/// Enamel::default() /// .ContentSecurityPolicy(CSP { /// sandbox: allow_forms | allow_same_origin, /// ..Default::default() @@ -249,16 +271,18 @@ const _: () = { } }; +/// # `sandbox` configuration for `enamel::CSP` +/// /// ## Example /// /// ```no_run /// use ohkami::prelude::*; -/// use ohkami::fang::helmet::{Helmet, CSP, sandbox::{allow_forms, allow_same_origin}}; +/// use ohkami::fang::enamel::{Enamel, CSP, sandbox::{allow_forms, allow_same_origin}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( -/// Helmet::default() +/// Enamel::default() /// .ContentSecurityPolicy(CSP { /// sandbox: allow_forms | allow_same_origin, /// ..Default::default() @@ -318,21 +342,23 @@ pub mod sandbox { } } +/// # Source List configuration for `enamel::CSP` +/// /// ## Example /// /// ```no_run /// use ohkami::prelude::*; -/// use ohkami::fang::helmet::{Helmet, CSP, src::{self_origin, data}}; +/// use ohkami::fang::enamel::{Enamel, CSP, src::{self_origin, data}}; /// /// #[tokio::main] /// async fn main() { /// Ohkami::new(( -/// Helmet::default() +/// Enamel::default() /// .ContentSecurityPolicy(CSP { /// script_src: self_origin | data, /// ..Default::default() /// }), -/// "/hello".GET(|| async {"Hello, helmet!"}) +/// "/hello".GET(|| async {"Hello, enamel!"}) /// )).howl("localhost:3000").await /// } /// ``` @@ -470,10 +496,10 @@ mod test { use std::collections::HashSet; #[test] - fn helmet_set_headers() { + fn enamel_set_headers() { let t = Ohkami::new(( - Helmet::default(), - "/hello".GET(|| async {"Hello, helmet!"}), + Enamel::default(), + "/hello".GET(|| async {"Hello, enamel!"}), )).test(); crate::__rt__::testing::block_on(async { @@ -482,7 +508,7 @@ mod test { let req = TestRequest::GET("/hello"); let res = t.oneshot(req).await; assert_eq!(res.status().code(), 200); - assert_eq!(res.text().unwrap(), "Hello, helmet!"); + assert_eq!(res.text().unwrap(), "Hello, enamel!"); assert_eq!(res.headers().collect::>(), HashSet::from_iter([ ("Cross-Origin-Embedder-Policy", "require-corp"), ("Cross-Origin-Resource-Policy", "same-origin"), @@ -543,12 +569,12 @@ mod test { } #[test] - fn helmet_csp() { + fn enamel_csp() { use src::{self_origin, https, domain}; use sandbox::{allow_forms, allow_modals}; let t = Ohkami::new(( - Helmet::default() + Enamel::default() .ContentSecurityPolicy(CSP { default_src: self_origin | https | domain("*.example.com"), sandbox: allow_forms | allow_modals, @@ -556,7 +582,7 @@ mod test { ..Default::default() }), "/hello" - .GET(|| async {"Hello, helmet!"}), + .GET(|| async {"Hello, enamel!"}), )).test(); crate::__rt__::testing::block_on(async { @@ -564,7 +590,7 @@ mod test { let req = TestRequest::GET("/hello"); let res = t.oneshot(req).await; assert_eq!(res.status().code(), 200); - assert_eq!(res.text().unwrap(), "Hello, helmet!"); + assert_eq!(res.text().unwrap(), "Hello, enamel!"); assert_eq!(res.headers().collect::>(), HashSet::from_iter([ /* defaults */ ("Cross-Origin-Embedder-Policy", "require-corp"), @@ -585,13 +611,13 @@ mod test { } #[test] - fn helmet_disable_header() { + fn enamel_disable_header() { let t = Ohkami::new(( - Helmet::default() + Enamel::default() .CrossOriginEmbedderPolicy("") .CrossOriginResourcePolicy(""), "/hello" - .GET(|| async {"Hello, helmet!"}), + .GET(|| async {"Hello, enamel!"}), )).test(); crate::__rt__::testing::block_on(async { @@ -599,7 +625,7 @@ mod test { let req = TestRequest::GET("/hello"); let res = t.oneshot(req).await; assert_eq!(res.status().code(), 200); - assert_eq!(res.text().unwrap(), "Hello, helmet!"); + assert_eq!(res.text().unwrap(), "Hello, enamel!"); assert_eq!(res.headers().collect::>(), HashSet::from_iter([ /* ("Cross-Origin-Embedder-Policy", "require-corp"), */ /* ("Cross-Origin-Resource-Policy", "same-origin"), */ From 53f70451a05734752bcdd127d2ad6f38f5256195 Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 20:38:25 +0900 Subject: [PATCH 14/17] fix rename --- ohkami/src/fang/builtin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ohkami/src/fang/builtin.rs b/ohkami/src/fang/builtin.rs index d15e74a8c..44728923f 100644 --- a/ohkami/src/fang/builtin.rs +++ b/ohkami/src/fang/builtin.rs @@ -10,8 +10,8 @@ pub use jwt::{JWT, JWTToken}; mod context; pub use context::Context; -pub mod helmet; -pub use helmet::Helmet; +pub mod enamel; +pub use enamel::Enamel; #[cfg(feature="__rt_native__")] mod timeout; From 7e2586b93e399ce111813aa50e7f85f1d0f2b793 Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 20:41:06 +0900 Subject: [PATCH 15/17] update README --- README.md | 89 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 070df65e0..cf6e97ffa 100644 --- a/README.md +++ b/README.md @@ -284,48 +284,6 @@ async fn main() { ## Snippets -### Middlewares - -Ohkami's request handling system is called "**fang**s", and middlewares are implemented on this. - -*builtin fang* : `Context`, `CORS`, `JWT`, `BasicAuth`, `Timeout`, `Enamel` *( experimantal )* - -```rust,no_run -use ohkami::prelude::*; - -#[derive(Clone)] -struct GreetingFang(usize); - -/* utility trait; automatically impl `Fang` trait */ -impl FangAction for GreetingFang { - async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { - let Self(id) = self; - println!("[{id}] Welcome request!: {req:?}"); - Ok(()) - } - async fn back<'a>(&'a self, res: &'a mut Response) { - let Self(id) = self; - println!("[{id}] Go, response!: {res:?}"); - } -} - -#[tokio::main] -async fn main() { - Ohkami::new(( - // register fangs to a Ohkami - GreetingFang(1), - - "/hello" - .GET(|| async {"Hello, fangs!"}) - .POST(( - // register *local fangs* to a handler - GreetingFang(2), - || async {"I'm `POST /hello`!"} - )) - )).howl("localhost:3000").await -} -``` - ### Typed payload *builtin payload* : `JSON`, `Text`, `HTML`, `URLEncoded`, `Multipart` @@ -404,6 +362,53 @@ async fn search( } ``` +### Middlewares + +Ohkami's request handling system is called "**fang**s", and middlewares are implemented on this. + +*builtin fang* : + +- `Context` *( typed interaction with reuqest context )* +- `CORS`, `JWT`, `BasicAuth` +- `Timeout` *( native runtime )* +- `Enamel` *( experimantal; security headers )* + +```rust,no_run +use ohkami::prelude::*; + +#[derive(Clone)] +struct GreetingFang(usize); + +/* utility trait; automatically impl `Fang` trait */ +impl FangAction for GreetingFang { + async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> { + let Self(id) = self; + println!("[{id}] Welcome request!: {req:?}"); + Ok(()) + } + async fn back<'a>(&'a self, res: &'a mut Response) { + let Self(id) = self; + println!("[{id}] Go, response!: {res:?}"); + } +} + +#[tokio::main] +async fn main() { + Ohkami::new(( + // register fangs to a Ohkami + GreetingFang(1), + + "/hello" + .GET(|| async {"Hello, fangs!"}) + .POST(( + // register *local fangs* to a handler + GreetingFang(2), + || async {"I'm `POST /hello`!"} + )) + )).howl("localhost:3000").await +} +``` + ### Database connection management with `Context` ```rust,no_run From 56774cc837b87e7f117769d7cf71500983d06a5a Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 21:48:47 +0900 Subject: [PATCH 16/17] refactor around response headers --- ohkami/src/fang/builtin/enamel.rs | 18 ++++--- ohkami/src/ohkami/mod.rs | 4 +- ohkami/src/response/_test.rs | 42 ++++++++------- ohkami/src/response/_test_headers.rs | 43 ++++++++++----- ohkami/src/response/headers.rs | 10 +++- ohkami/src/response/mod.rs | 79 +++++++++------------------- ohkami/src/router/final.rs | 35 +++++++----- ohkami/src/ws/native.rs | 6 ++- ohkami/src/ws/worker.rs | 9 ++-- 9 files changed, 131 insertions(+), 115 deletions(-) diff --git a/ohkami/src/fang/builtin/enamel.rs b/ohkami/src/fang/builtin/enamel.rs index 9863e1227..bdcce034c 100644 --- a/ohkami/src/fang/builtin/enamel.rs +++ b/ohkami/src/fang/builtin/enamel.rs @@ -509,7 +509,7 @@ mod test { let res = t.oneshot(req).await; assert_eq!(res.status().code(), 200); assert_eq!(res.text().unwrap(), "Hello, enamel!"); - assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + assert_eq!(res.headers().filter(|(h, _)| *h != "Date").collect::>(), HashSet::from_iter([ ("Cross-Origin-Embedder-Policy", "require-corp"), ("Cross-Origin-Resource-Policy", "same-origin"), ("Referrer-Policy", "no-referrer"), @@ -528,13 +528,15 @@ mod test { let res = t.oneshot(req).await; assert_eq!(res.status().code(), 404); assert_eq!(res.text(), None); - assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + assert_eq!(res.headers().filter(|(h, _)| *h != "Date").collect::>(), HashSet::from_iter([ ("Cross-Origin-Embedder-Policy", "require-corp"), ("Cross-Origin-Resource-Policy", "same-origin"), ("Referrer-Policy", "no-referrer"), ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), ("X-Content-Type-Options", "nosniff"), ("X-Frame-Options", "SAMEORIGIN"), + + ("Content-Length", "0"), ])); } { @@ -542,13 +544,15 @@ mod test { let res = t.oneshot(req).await; assert_eq!(res.status().code(), 404); assert_eq!(res.text(), None); - assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + assert_eq!(res.headers().filter(|(h, _)| *h != "Date").collect::>(), HashSet::from_iter([ ("Cross-Origin-Embedder-Policy", "require-corp"), ("Cross-Origin-Resource-Policy", "same-origin"), ("Referrer-Policy", "no-referrer"), ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), ("X-Content-Type-Options", "nosniff"), ("X-Frame-Options", "SAMEORIGIN"), + + ("Content-Length", "0"), ])); } { @@ -556,13 +560,15 @@ mod test { let res = t.oneshot(req).await; assert_eq!(res.status().code(), 404); assert_eq!(res.text(), None); - assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + assert_eq!(res.headers().filter(|(h, _)| *h != "Date").collect::>(), HashSet::from_iter([ ("Cross-Origin-Embedder-Policy", "require-corp"), ("Cross-Origin-Resource-Policy", "same-origin"), ("Referrer-Policy", "no-referrer"), ("Strict-Transport-Security", "max-age=15552000; includeSubDomains"), ("X-Content-Type-Options", "nosniff"), ("X-Frame-Options", "SAMEORIGIN"), + + ("Content-Length", "0"), ])); } }); @@ -591,7 +597,7 @@ mod test { let res = t.oneshot(req).await; assert_eq!(res.status().code(), 200); assert_eq!(res.text().unwrap(), "Hello, enamel!"); - assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + assert_eq!(res.headers().filter(|(h, _)| *h != "Date").collect::>(), HashSet::from_iter([ /* defaults */ ("Cross-Origin-Embedder-Policy", "require-corp"), ("Cross-Origin-Resource-Policy", "same-origin"), @@ -626,7 +632,7 @@ mod test { let res = t.oneshot(req).await; assert_eq!(res.status().code(), 200); assert_eq!(res.text().unwrap(), "Hello, enamel!"); - assert_eq!(res.headers().collect::>(), HashSet::from_iter([ + assert_eq!(res.headers().filter(|(h, _)| *h != "Date").collect::>(), HashSet::from_iter([ /* ("Cross-Origin-Embedder-Policy", "require-corp"), */ /* ("Cross-Origin-Resource-Policy", "same-origin"), */ ("Referrer-Policy", "no-referrer"), diff --git a/ohkami/src/ohkami/mod.rs b/ohkami/src/ohkami/mod.rs index 07dc1ddaf..cf0d3cc5a 100644 --- a/ohkami/src/ohkami/mod.rs +++ b/ohkami/src/ohkami/mod.rs @@ -446,9 +446,7 @@ impl Ohkami { let (router, _) = self.into_router().finalize(); #[cfg(feature="DEBUG")] ::worker::console_debug!("Done `self.router.finalize`"); - let mut res = router.handle(&mut ohkami_req).await; - res.complete(); - res + router.handle(&mut ohkami_req).await } Err(e) => {#[cfg(feature="DEBUG")] ::worker::console_debug!("`take_over` returned an error response: {e:?}"); e diff --git a/ohkami/src/response/_test.rs b/ohkami/src/response/_test.rs index afcbc45a3..20d4dc723 100644 --- a/ohkami/src/response/_test.rs +++ b/ohkami/src/response/_test.rs @@ -8,9 +8,11 @@ macro_rules! response_dump { } } -macro_rules! assert_bytes_eq { +macro_rules! assert_response_bytes_eq { ($res:expr, $expected:expr) => { { + $res.complete(); + let mut res_bytes = Vec::new(); crate::__rt__::testing::block_on( $res.send(&mut res_bytes) @@ -32,8 +34,8 @@ macro_rules! assert_bytes_eq { #[test] fn test_response_into_bytes() { - let res = Response::NoContent(); - assert_bytes_eq!(res, response_dump!("\ + let mut res = Response::NoContent(); + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 204 No Content\r\n\ Date: {NOW}\r\n\ \r\n\ @@ -41,15 +43,15 @@ fn test_response_into_bytes() { let mut res = Response::NoContent(); res.headers.set().Server("ohkami"); - assert_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 204 No Content\r\n\ - Server: ohkami\r\n\ Date: {NOW}\r\n\ + Server: ohkami\r\n\ \r\n\ ").into_bytes()); - let res = Response::NotFound(); - assert_bytes_eq!(res, response_dump!("\ + let mut res = Response::NotFound(); + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 404 Not Found\r\n\ Date: {NOW}\r\n\ Content-Length: 0\r\n\ @@ -60,11 +62,11 @@ fn test_response_into_bytes() { res.headers.set() .Server("ohkami") .x("Hoge-Header", "Something-Custom"); - assert_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 404 Not Found\r\n\ - Server: ohkami\r\n\ Date: {NOW}\r\n\ Content-Length: 0\r\n\ + Server: ohkami\r\n\ Hoge-Header: Something-Custom\r\n\ \r\n\ ").into_bytes()); @@ -75,11 +77,11 @@ fn test_response_into_bytes() { .x("Hoge-Header", "Something-Custom") .SetCookie("id", "42", |d|d.Path("/").SameSiteLax()) .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()); - assert_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 404 Not Found\r\n\ - Server: ohkami\r\n\ Date: {NOW}\r\n\ Content-Length: 0\r\n\ + Server: ohkami\r\n\ Hoge-Header: Something-Custom\r\n\ Set-Cookie: id=42; Path=/; SameSite=Lax\r\n\ Set-Cookie: name=John; Path=/where; SameSite=Strict\r\n\ @@ -92,12 +94,12 @@ fn test_response_into_bytes() { .x("Hoge-Header", "Something-Custom") .SetCookie("id", "42", |d|d.Path("/").SameSiteLax()) .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()); - assert_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 404 Not Found\r\n\ - Content-Type: text/plain; charset=UTF-8\r\n\ - Server: ohkami\r\n\ Date: {NOW}\r\n\ Content-Length: 11\r\n\ + Content-Type: text/plain; charset=UTF-8\r\n\ + Server: ohkami\r\n\ Hoge-Header: Something-Custom\r\n\ Set-Cookie: id=42; Path=/; SameSite=Lax\r\n\ Set-Cookie: name=John; Path=/where; SameSite=Strict\r\n\ @@ -137,7 +139,7 @@ fn test_stream_response() { Repeat { f, n, count: 0 } } - let res = Response::OK() + let mut res = Response::OK() .with_stream( repeat_by(3, |i| format!("This is message#{i} !")) ) @@ -146,13 +148,13 @@ fn test_stream_response() { .x("is-stream", "true") .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()) ); - assert_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 200 OK\r\n\ + Date: {NOW}\r\n\ Content-Type: text/event-stream\r\n\ Cache-Control: no-cache, must-revalidate\r\n\ Transfer-Encoding: chunked\r\n\ Server: ohkami\r\n\ - Date: {NOW}\r\n\ is-stream: true\r\n\ Set-Cookie: name=John; Path=/where; SameSite=Strict\r\n\ \r\n\ @@ -172,7 +174,7 @@ fn test_stream_response() { \r\n\ ").into_bytes()); - let res = Response::OK() + let mut res = Response::OK() .with_stream( repeat_by(3, |i| format!("This is message#{i}\nです")) ) @@ -181,13 +183,13 @@ fn test_stream_response() { .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()) .x("is-stream", "true") ); - assert_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, response_dump!("\ HTTP/1.1 200 OK\r\n\ + Date: {NOW}\r\n\ Content-Type: text/event-stream\r\n\ Cache-Control: no-cache, must-revalidate\r\n\ Transfer-Encoding: chunked\r\n\ Server: ohkami\r\n\ - Date: {NOW}\r\n\ is-stream: true\r\n\ Set-Cookie: name=John; Path=/where; SameSite=Strict\r\n\ \r\n\ diff --git a/ohkami/src/response/_test_headers.rs b/ohkami/src/response/_test_headers.rs index 8a2b38b26..438d348d9 100644 --- a/ohkami/src/response/_test_headers.rs +++ b/ohkami/src/response/_test_headers.rs @@ -3,6 +3,11 @@ use crate::header::{append, SameSitePolicy, SetCookie}; use super::ResponseHeaders; +macro_rules! headers_dump { + ($dump:literal) => { + format!($dump, NOW = ::ohkami_lib::imf_fixdate(crate::util::unix_timestamp())) + } +} #[test] fn insert_and_write() { let mut h = ResponseHeaders::new(); @@ -10,7 +15,12 @@ use super::ResponseHeaders; { let mut buf = Vec::new(); h._write_to(&mut buf); - assert_eq!(std::str::from_utf8(&buf).unwrap(), "Server: A\r\n\r\n"); + assert_eq!(std::str::from_utf8(&buf).unwrap(), headers_dump!("\ + Date: {NOW}\r\n\ + Content-Length: 0\r\n\ + Server: A\r\n\ + \r\n\ + ")); } let mut h = ResponseHeaders::new(); @@ -20,12 +30,13 @@ use super::ResponseHeaders; { let mut buf = Vec::new(); h._write_to(&mut buf); - assert_eq!(std::str::from_utf8(&buf).unwrap(), "\ + assert_eq!(std::str::from_utf8(&buf).unwrap(), headers_dump!("\ + Date: {NOW}\r\n\ + Content-Length: 42\r\n\ Server: B\r\n\ Content-Type: text/html\r\n\ - Content-Length: 42\r\n\ \r\n\ - "); + ")); } } @@ -37,10 +48,12 @@ use super::ResponseHeaders; { let mut buf = Vec::new(); h._write_to(&mut buf); - assert_eq!(std::str::from_utf8(&buf).unwrap(), "\ + assert_eq!(std::str::from_utf8(&buf).unwrap(), headers_dump!("\ + Date: {NOW}\r\n\ + Content-Length: 0\r\n\ Server: X\r\n\ \r\n\ - "); + ")); } h.set().Server(append("Y")); @@ -48,10 +61,12 @@ use super::ResponseHeaders; { let mut buf = Vec::new(); h._write_to(&mut buf); - assert_eq!(std::str::from_utf8(&buf).unwrap(), "\ + assert_eq!(std::str::from_utf8(&buf).unwrap(), headers_dump!("\ + Date: {NOW}\r\n\ + Content-Length: 0\r\n\ Server: X, Y\r\n\ \r\n\ - "); + ")); } } @@ -63,10 +78,12 @@ use super::ResponseHeaders; { let mut buf = Vec::new(); h._write_to(&mut buf); - assert_eq!(std::str::from_utf8(&buf).unwrap(), "\ + assert_eq!(std::str::from_utf8(&buf).unwrap(), headers_dump!("\ + Date: {NOW}\r\n\ + Content-Length: 0\r\n\ Custom-Header: A\r\n\ \r\n\ - "); + ")); } h.set().x("Custom-Header", append("B")); @@ -74,10 +91,12 @@ use super::ResponseHeaders; { let mut buf = Vec::new(); h._write_to(&mut buf); - assert_eq!(std::str::from_utf8(&buf).unwrap(), "\ + assert_eq!(std::str::from_utf8(&buf).unwrap(), headers_dump!("\ + Date: {NOW}\r\n\ + Content-Length: 0\r\n\ Custom-Header: A, B\r\n\ \r\n\ - "); + ")); } } diff --git a/ohkami/src/response/headers.rs b/ohkami/src/response/headers.rs index 21ff1cd4d..26a9b578a 100644 --- a/ohkami/src/response/headers.rs +++ b/ohkami/src/response/headers.rs @@ -418,12 +418,18 @@ impl Headers { impl Headers { #[inline] pub(crate) fn new() -> Self { - Self { + let mut this = Self { standard: IndexMap::new(), custom: None, setcookie: None, size: "\r\n".len(), - } + }; + + this.set() + .Date(ohkami_lib::imf_fixdate(crate::util::unix_timestamp())) + .ContentLength("0"); + + this } #[cfg(feature="DEBUG")] #[doc(hidden)] diff --git a/ohkami/src/response/mod.rs b/ohkami/src/response/mod.rs index 84d4acac0..9b5145dbc 100644 --- a/ohkami/src/response/mod.rs +++ b/ohkami/src/response/mod.rs @@ -16,7 +16,7 @@ pub use into_response::{IntoResponse, IntoBody}; #[cfg(test)] mod _test_headers; use std::borrow::Cow; -use ohkami_lib::{CowSlice, Slice, num}; +use ohkami_lib::{CowSlice, Slice}; #[cfg(feature="__rt_native__")] use crate::__rt__::AsyncWrite; @@ -122,39 +122,24 @@ impl Response { } #[cfg(feature="__rt__")] - /// Complete HTTP spec - #[inline(always)] + /// complete HTTP spec + /// + /// should be called, like, just after router's handling pub(crate) fn complete(&mut self) { - self.headers.set().Date(::ohkami_lib::imf_fixdate(crate::util::unix_timestamp())); - - match &self.content { - Content::None => { - match self.status { - Status::NoContent => self.headers.set() - .ContentLength(None), - _ => self.headers.set() - .ContentLength("0") - }; + if matches!((&self.content, &self.status), + | (_, Status::NoContent) + | (Content::Stream(_), _) + | (Content::WebSocket(_), _) + ) { + if !/* not */self.headers.ContentLength().is_none() { + self.headers.set().ContentLength(None); } - Content::Payload(bytes) => { - self.headers.set() - .ContentLength(ohkami_lib::num::itoa(bytes.len())); + if self.status == Status::NoContent + && !/* not */matches!(self.content, Content::None) { + self.content = Content::None; } - - #[cfg(feature="sse")] - Content::Stream(_) => { - self.headers.set() - .ContentLength(None); - } - - #[cfg(not(feature="rt_lambda"/* currently */))] - #[cfg(feature="ws")] - Content::WebSocket(_) => { - self.headers.set() - .ContentLength(None); - } - }; + } } } @@ -182,10 +167,10 @@ impl Response { content_type: &'static str, content: impl Into>, ) { - let content = content.into(); + let content: Cow<'static, [u8]> = content.into(); self.headers.set() .ContentType(content_type) - .ContentLength(num::itoa(content.len())); + .ContentLength(ohkami_lib::num::itoa(content.len())); self.content = Content::Payload(content.into()); } #[inline] @@ -206,7 +191,7 @@ impl Response { self.headers.set() .ContentType("text/plain; charset=UTF-8") - .ContentLength(num::itoa(body.len())); + .ContentLength(ohkami_lib::num::itoa(body.len())); self.content = Content::Payload(match body { Cow::Borrowed(str) => CowSlice::Ref(Slice::from_bytes(str.as_bytes())), Cow::Owned(string) => CowSlice::Own(string.into_bytes().into()), @@ -223,7 +208,7 @@ impl Response { self.headers.set() .ContentType("text/html; charset=UTF-8") - .ContentLength(num::itoa(body.len())); + .ContentLength(ohkami_lib::num::itoa(body.len())); self.content = Content::Payload(match body { Cow::Borrowed(str) => CowSlice::Ref(Slice::from_bytes(str.as_bytes())), Cow::Owned(string) => CowSlice::Own(string.into_bytes().into()), @@ -239,7 +224,7 @@ impl Response { let body = ::serde_json::to_vec(&json).unwrap(); self.headers.set() .ContentType("application/json") - .ContentLength(num::itoa(body.len())); + .ContentLength(ohkami_lib::num::itoa(body.len())); self.content = Content::Payload(body.into()); } #[inline(always)] @@ -256,7 +241,8 @@ impl Response { }; self.headers.set() - .ContentType("application/json"); + .ContentType("application/json") + .ContentLength(ohkami_lib::num::itoa(body.len())); self.content = Content::Payload(body.into()); } /// SAFETY: argument `json_lit` must be **valid JSON** @@ -288,6 +274,7 @@ impl Response { stream: std::pin::Pin + Send>> ) { self.headers.set() + .ContentLength(None) .ContentType("text/event-stream") .CacheControl("no-cache, must-revalidate") .TransferEncoding("chunked"); @@ -295,21 +282,6 @@ impl Response { } } -#[cfg(feature="ws")] -/// Of course here no method for rt_lambda exists; see x_lambda.rs -impl Response { - #[cfg(feature="__rt_native__")] - pub(crate) fn with_websocket(mut self, ws: mews::WebSocket) -> Self { - self.content = Content::WebSocket(ws); - self - } - #[cfg(feature="rt_worker")] - pub(crate) fn with_websocket(mut self, ws: worker::WebSocket) -> Self { - self.content = Content::WebSocket(ws); - self - } -} - #[cfg(feature="__rt_native__")] pub(super) enum Upgrade { None, @@ -327,11 +299,10 @@ impl Upgrade { #[cfg(feature="__rt_native__")] impl Response { #[cfg_attr(not(feature="sse"), inline)] - pub(crate) async fn send(mut self, + pub(crate) async fn send( + self, conn: &mut (impl AsyncWrite + Unpin) ) -> Upgrade { - self.complete(); - match self.content { Content::None => { let mut buf = Vec::::with_capacity( diff --git a/ohkami/src/router/final.rs b/ohkami/src/router/final.rs index ae65c8a96..663ac362a 100644 --- a/ohkami/src/router/final.rs +++ b/ohkami/src/router/final.rs @@ -34,21 +34,30 @@ enum Pattern { impl Router { pub(crate) async fn handle(&self, req: &mut Request) -> Response { - match req.method { - Method::GET => &self.GET, - Method::PUT => &self.PUT, - Method::POST => &self.POST, - Method::PATCH => &self.PATCH, - Method::DELETE => &self.DELETE, - Method::OPTIONS => &self.OPTIONS, - Method::HEAD => return { - let mut res = self.GET.search(&mut req.path).call_bite(req).await; - {/* not `res.drop_content()` to leave `Content-Type`, `Content-Length` */ + let mut res = 'handle: { + (match req.method { + Method::GET => &self.GET, + Method::PUT => &self.PUT, + Method::POST => &self.POST, + Method::PATCH => &self.PATCH, + Method::DELETE => &self.DELETE, + Method::OPTIONS => &self.OPTIONS, + + Method::HEAD => { + let mut res = self.GET.search(&mut req.path).call_bite(req).await; + + /* not `res.drop_content()` to keep `Content-Type`, `Content-Length` */ res.content = Content::None; + + break 'handle res } - res - } - }.search(&mut req.path).call_bite(req).await + + }).search(&mut req.path).call_bite(req).await + }; + + res.complete(); + + res } #[cfg(feature="openapi")] diff --git a/ohkami/src/ws/native.rs b/ohkami/src/ws/native.rs index 3f91ccf32..6b5fe7c5b 100644 --- a/ohkami/src/ws/native.rs +++ b/ohkami/src/ws/native.rs @@ -116,11 +116,13 @@ pub struct WebSocket crate::Response { - crate::Response::SwitchingProtocols().with_headers(|h|h + let mut res = crate::Response::SwitchingProtocols(); + res.content = crate::response::Content::WebSocket(self.session); + res.with_headers(|h|h .Connection("Upgrade") .Upgrade("websocket") .SecWebSocketAccept(self.sign) - ).with_websocket(self.session) + ) } #[cfg(feature="openapi")] diff --git a/ohkami/src/ws/worker.rs b/ohkami/src/ws/worker.rs index 7d074d0d2..29cb664ea 100644 --- a/ohkami/src/ws/worker.rs +++ b/ohkami/src/ws/worker.rs @@ -262,9 +262,12 @@ pub mod split { pub struct WebSocket(Session); impl crate::IntoResponse for WebSocket { fn into_response(self) -> crate::Response { - crate::Response::SwitchingProtocols().with_websocket(self.0) - // let `worker` crate and Cloudflare Workers to do around - // headers and something other + let mut res = crate::Response::SwitchingProtocols(); + res.content = crate::response::Content::Websocket(self.0); + res /* + let `worker` crate and Cloudflare Workers to do around + headers and something other + */ } #[cfg(feature="openapi")] From 514e51a99b5223e33f4c70af69f04196a1107202 Mon Sep 17 00:00:00 2001 From: kanarus Date: Fri, 14 Feb 2025 22:13:15 +0900 Subject: [PATCH 17/17] fix tests & cfgs --- ohkami/src/fang/builtin/enamel.rs | 1 + ohkami/src/response/_test.rs | 49 ++++++++++++++-------------- ohkami/src/response/_test_headers.rs | 1 + ohkami/src/response/mod.rs | 32 +++++++++++------- ohkami/src/ws/mod.rs | 2 ++ ohkami/src/ws/worker.rs | 2 +- 6 files changed, 50 insertions(+), 37 deletions(-) diff --git a/ohkami/src/fang/builtin/enamel.rs b/ohkami/src/fang/builtin/enamel.rs index bdcce034c..2edd521c6 100644 --- a/ohkami/src/fang/builtin/enamel.rs +++ b/ohkami/src/fang/builtin/enamel.rs @@ -489,6 +489,7 @@ pub mod src { #[cfg(test)] +#[cfg(feature="__rt_native__")] mod test { use super::*; use crate::prelude::*; diff --git a/ohkami/src/response/_test.rs b/ohkami/src/response/_test.rs index 20d4dc723..fcef15956 100644 --- a/ohkami/src/response/_test.rs +++ b/ohkami/src/response/_test.rs @@ -2,30 +2,29 @@ use crate::Response; -macro_rules! response_dump { - ($dump:literal) => { - format!($dump, NOW = ::ohkami_lib::imf_fixdate(crate::util::unix_timestamp())) - } -} - macro_rules! assert_response_bytes_eq { - ($res:expr, $expected:expr) => { + ($res:expr, $expected:literal) => { { $res.complete(); + /* avoid flakiness of 1 sec difference */ + let now = $res.headers.Date() + .expect("No `Date` header in res") + .to_string(/* `$res` moves by `.send` */); + let mut res_bytes = Vec::new(); crate::__rt__::testing::block_on( $res.send(&mut res_bytes) ); - if res_bytes != $expected { + if res_bytes != format!($expected, NOW = now).into_bytes() { panic!("\n\ [got]\n\ {}\n\ [expected]\n\ {}\n", (res_bytes).escape_ascii(), - ($expected).escape_ascii(), + $expected, ) } } @@ -35,41 +34,41 @@ macro_rules! assert_response_bytes_eq { #[test] fn test_response_into_bytes() { let mut res = Response::NoContent(); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 204 No Content\r\n\ Date: {NOW}\r\n\ \r\n\ - ").into_bytes()); + "); let mut res = Response::NoContent(); res.headers.set().Server("ohkami"); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 204 No Content\r\n\ Date: {NOW}\r\n\ Server: ohkami\r\n\ \r\n\ - ").into_bytes()); + "); let mut res = Response::NotFound(); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 404 Not Found\r\n\ Date: {NOW}\r\n\ Content-Length: 0\r\n\ \r\n\ - ").into_bytes()); + "); let mut res = Response::NotFound(); res.headers.set() .Server("ohkami") .x("Hoge-Header", "Something-Custom"); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 404 Not Found\r\n\ Date: {NOW}\r\n\ Content-Length: 0\r\n\ Server: ohkami\r\n\ Hoge-Header: Something-Custom\r\n\ \r\n\ - ").into_bytes()); + "); let mut res = Response::NotFound(); res.headers.set() @@ -77,7 +76,7 @@ fn test_response_into_bytes() { .x("Hoge-Header", "Something-Custom") .SetCookie("id", "42", |d|d.Path("/").SameSiteLax()) .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 404 Not Found\r\n\ Date: {NOW}\r\n\ Content-Length: 0\r\n\ @@ -86,7 +85,7 @@ fn test_response_into_bytes() { Set-Cookie: id=42; Path=/; SameSite=Lax\r\n\ Set-Cookie: name=John; Path=/where; SameSite=Strict\r\n\ \r\n\ - ").into_bytes()); + "); let mut res = Response::NotFound().with_text("sample text"); res.headers.set() @@ -94,7 +93,7 @@ fn test_response_into_bytes() { .x("Hoge-Header", "Something-Custom") .SetCookie("id", "42", |d|d.Path("/").SameSiteLax()) .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 404 Not Found\r\n\ Date: {NOW}\r\n\ Content-Length: 11\r\n\ @@ -105,7 +104,7 @@ fn test_response_into_bytes() { Set-Cookie: name=John; Path=/where; SameSite=Strict\r\n\ \r\n\ sample text\ - ").into_bytes()); + "); } #[cfg(feature="sse")] @@ -148,7 +147,7 @@ fn test_stream_response() { .x("is-stream", "true") .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()) ); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 200 OK\r\n\ Date: {NOW}\r\n\ Content-Type: text/event-stream\r\n\ @@ -172,7 +171,7 @@ fn test_stream_response() { \r\n\ 0\r\n\ \r\n\ - ").into_bytes()); + "); let mut res = Response::OK() .with_stream( @@ -183,7 +182,7 @@ fn test_stream_response() { .SetCookie("name", "John", |d|d.Path("/where").SameSiteStrict()) .x("is-stream", "true") ); - assert_response_bytes_eq!(res, response_dump!("\ + assert_response_bytes_eq!(res, "\ HTTP/1.1 200 OK\r\n\ Date: {NOW}\r\n\ Content-Type: text/event-stream\r\n\ @@ -210,5 +209,5 @@ fn test_stream_response() { \r\n\ 0\r\n\ \r\n\ - ").into_bytes()); + "); } diff --git a/ohkami/src/response/_test_headers.rs b/ohkami/src/response/_test_headers.rs index 438d348d9..860f59fa6 100644 --- a/ohkami/src/response/_test_headers.rs +++ b/ohkami/src/response/_test_headers.rs @@ -1,4 +1,5 @@ #![cfg(test)] +#![cfg(feature="__rt_native__")] use crate::header::{append, SameSitePolicy, SetCookie}; use super::ResponseHeaders; diff --git a/ohkami/src/response/mod.rs b/ohkami/src/response/mod.rs index 9b5145dbc..c2c5e770d 100644 --- a/ohkami/src/response/mod.rs +++ b/ohkami/src/response/mod.rs @@ -126,19 +126,29 @@ impl Response { /// /// should be called, like, just after router's handling pub(crate) fn complete(&mut self) { - if matches!((&self.content, &self.status), - | (_, Status::NoContent) - | (Content::Stream(_), _) - | (Content::WebSocket(_), _) - ) { - if !/* not */self.headers.ContentLength().is_none() { - self.headers.set().ContentLength(None); + match (&self.content, &self.status) { + (_, Status::NoContent) => { + if !/* not */self.headers.ContentLength().is_none() { + self.headers.set().ContentLength(None); + } + if !/* not */matches!(self.content, Content::None) { + self.content = Content::None; + } } - - if self.status == Status::NoContent - && !/* not */matches!(self.content, Content::None) { - self.content = Content::None; + #[cfg(feature="sse")] + (Content::Stream(_), _) => { + if !/* not */self.headers.ContentLength().is_none() { + self.headers.set().ContentLength(None); + } + } + #[cfg(not(feature="rt_lambda"/* currently */))] + #[cfg(all(feature="ws", feature="__rt__"))] + (Content::WebSocket(_), _) => { + if !/* not */self.headers.ContentLength().is_none() { + self.headers.set().ContentLength(None); + } } + _ => (/* let it go by user's responsibility */) } } } diff --git a/ohkami/src/ws/mod.rs b/ohkami/src/ws/mod.rs index 673a6f6b8..da2ed3d40 100644 --- a/ohkami/src/ws/mod.rs +++ b/ohkami/src/ws/mod.rs @@ -1,3 +1,5 @@ +#![cfg(feature="ws")] + #[cfg(feature="__rt_native__")] mod native; #[cfg(feature="__rt_native__")] diff --git a/ohkami/src/ws/worker.rs b/ohkami/src/ws/worker.rs index 29cb664ea..ca77f1dd3 100644 --- a/ohkami/src/ws/worker.rs +++ b/ohkami/src/ws/worker.rs @@ -263,7 +263,7 @@ pub struct WebSocket(Session); impl crate::IntoResponse for WebSocket { fn into_response(self) -> crate::Response { let mut res = crate::Response::SwitchingProtocols(); - res.content = crate::response::Content::Websocket(self.0); + res.content = crate::response::Content::WebSocket(self.0); res /* let `worker` crate and Cloudflare Workers to do around headers and something other