├── .gitignore ├── macros ├── Cargo.toml ├── LICENSE-MIT ├── src │ └── lib.rs └── LICENSE-APACHE ├── src ├── storage.rs ├── lib.rs ├── middleware.rs ├── attributes.rs └── cookies.rs ├── Cargo.toml ├── LICENSE-MIT ├── .github └── workflows │ └── ci.yml ├── tests └── middleware.rs ├── README.md ├── examples └── cookiebox.rs └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cookiebox-macros" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["Mohammed (Zack) Salah "] 6 | description = "macro Implementation for cookiebox crate" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/MSalah73/cookiebox" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | quote = "1.0.35" 15 | syn = { version = "2.0.87", features = ["full"] } -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use biscotti::{RequestCookies, ResponseCookies}; 4 | 5 | /// Holds a collection of both request and response cookies 6 | #[derive(Clone)] 7 | pub struct Storage<'s> { 8 | pub(crate) request_storage: Rc>>, 9 | pub(crate) response_storage: Rc>>, 10 | } 11 | impl Storage<'_> { 12 | pub(crate) fn new() -> Self { 13 | Storage { 14 | request_storage: Rc::new(RefCell::new(RequestCookies::new())), 15 | response_storage: Rc::new(RefCell::new(ResponseCookies::new())), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cookiebox" 3 | version = "0.3.0" 4 | edition = "2024" 5 | authors = ["Mohammed (Zack) Salah "] 6 | description = "A type safe cookie management crate" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/MSalah73/cookiebox" 9 | 10 | [workspace] 11 | members = ["macros",] 12 | 13 | [dependencies] 14 | cookiebox-macros = { version = "0.2.0", path = "macros"} 15 | biscotti = "0.4.0" 16 | serde_json = "1.0.132" 17 | serde = { version = "1.0.215", features = ["derive"]} 18 | anyhow = "1.0.93" 19 | thiserror = "2.0.3" 20 | actix-web = { version = "4.9", features = ["macros"], default-features = false} 21 | actix-utils = "3.0.1" -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Mohammed (Zack) Salah 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /macros/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Mohammed (Zack) Salah 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository last commit 16 | uses: actions/checkout@v4 17 | 18 | - name: Install the rust toolchain 19 | uses: dtolnay/rust-toolchain@stable 20 | 21 | - name: Rust Cache Action 22 | uses: Swatinem/rust-cache@v2 23 | 24 | - name: Run tests 25 | run: cargo test 26 | 27 | fmt: 28 | name: Rustfmt 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: dtolnay/rust-toolchain@stable 33 | with: 34 | components: rustfmt 35 | 36 | - name: Rust Cache Action 37 | uses: Swatinem/rust-cache@v2 38 | 39 | - name: Enforce formatting 40 | run: cargo fmt --check 41 | 42 | clippy: 43 | name: Clippy 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - name: Checkout repository last commit 48 | uses: actions/checkout@v4 49 | 50 | - name: Install the rust toolchain 51 | uses: dtolnay/rust-toolchain@stable 52 | with: 53 | components: clippy 54 | 55 | - name: Rust Cache Action 56 | uses: Swatinem/rust-cache@v2 57 | 58 | - name: Linting 59 | run: cargo clippy -- -D warnings 60 | 61 | readme-check: 62 | name: Readme check 63 | runs-on: ubuntu-latest 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: dtolnay/rust-toolchain@stable 68 | 69 | - name: Rust Cache Action 70 | uses: Swatinem/rust-cache@v2 71 | 72 | - name: Check if the README is up to date. 73 | run: | 74 | cargo install cargo-rdme 75 | cargo rdme --check -------------------------------------------------------------------------------- /tests/middleware.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{App, HttpMessage, HttpResponse, test, web}; 2 | use cookiebox::cookiebox_macros::{FromRequest, cookie}; 3 | use cookiebox::cookies::{Cookie, CookieName, IncomingConfig, OutgoingConfig}; 4 | use cookiebox::{Attributes, CookieMiddleware, Processor, ProcessorConfig, SameSite}; 5 | 6 | #[cookie(name = "Type A")] 7 | pub struct TypeA; 8 | impl IncomingConfig for TypeA { 9 | type Get = String; 10 | } 11 | impl OutgoingConfig for TypeA { 12 | type Insert = String; 13 | 14 | fn attributes<'c>() -> Attributes<'c> { 15 | Attributes::new().same_site(SameSite::Lax).http_only(true) 16 | } 17 | } 18 | 19 | #[derive(FromRequest)] 20 | pub struct CookieCollection<'c>(Cookie<'c, TypeA>); 21 | 22 | async fn register_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 23 | cookie.0.insert("id".to_string()); 24 | HttpResponse::Ok().finish() 25 | } 26 | async fn get_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 27 | let cookie = cookie.0.get().expect("Unable to get cookie"); 28 | HttpResponse::Ok().json(cookie) 29 | } 30 | async fn get_all_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 31 | let cookie = cookie.0.get_all().expect("Unable to get cookies"); 32 | HttpResponse::Ok().json(cookie) 33 | } 34 | async fn remove_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 35 | cookie.0.remove(); 36 | HttpResponse::Ok().finish() 37 | } 38 | 39 | #[actix_web::test] 40 | async fn cookie_middleware_tests() -> std::io::Result<()> { 41 | let processor: Processor = ProcessorConfig::default().into(); 42 | let app = test::init_service( 43 | App::new() 44 | .wrap(CookieMiddleware::new(processor.clone())) 45 | .route("/register", web::post().to(register_cookie)) 46 | .route("/get", web::post().to(get_cookie)) 47 | .route("/get-all", web::post().to(get_all_cookie)) 48 | .route("/remove", web::post().to(remove_cookie)), 49 | ) 50 | .await; 51 | 52 | // registering cookies to the browser 53 | let request = test::TestRequest::post().uri("/register").to_request(); 54 | let response = test::call_service(&app, request).await; 55 | let cookie_header = response 56 | .headers() 57 | .get(actix_web::http::header::SET_COOKIE) 58 | .expect("Cookie header not found") 59 | .to_str() 60 | .expect("Unable to stringify cookie header"); 61 | 62 | assert_eq!(cookie_header, "Type%20A=%22id%22; HttpOnly; SameSite=Lax"); 63 | 64 | // getting back cookies from the browser 65 | let cookie_header = "Type%20A=%22id%22"; 66 | let request = test::TestRequest::post() 67 | .insert_header((actix_web::http::header::COOKIE, cookie_header)) 68 | .uri("/get") 69 | .to_request(); 70 | let response = test::call_service(&app, request).await; 71 | let body_str: String = test::read_body_json(response).await; 72 | 73 | assert_eq!(body_str, "id"); 74 | 75 | // getting back a list of cookies with same name from the browser 76 | let cookie_header = "Type%20A=%22id%22; Type%20A=%22id2%22;"; 77 | let request = test::TestRequest::post() 78 | .insert_header((actix_web::http::header::COOKIE, cookie_header)) 79 | .uri("/get-all") 80 | .to_request(); 81 | let response = test::call_service(&app, request).await; 82 | let body_vec: Vec = test::read_body_json(response).await; 83 | 84 | assert_eq!(body_vec, vec!["id", "id2"]); 85 | 86 | // remove cookies from the user browser 87 | let request = test::TestRequest::post().uri("/remove").to_request(); 88 | let response = test::call_service(&app, request).await; 89 | let cookie_header = response 90 | .headers() 91 | .get(actix_web::http::header::SET_COOKIE) 92 | .expect("Cookie header not found") 93 | .to_str() 94 | .expect("Unable to stringify cookie header"); 95 | 96 | assert_eq!( 97 | cookie_header, 98 | "Type%20A=; Expires=Thu, 01 Jan 1970 00:00:00 GMT" 99 | ); 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A type safe cookie management crate for the Actix Web framework. 2 | //! 3 | //! Cookiebox provides a type safe and flexible approach to managing cookies in Actix web based applications. 4 | //! It allows you to define, configure, and manage cookies with minimal boilerplate. 5 | //! 6 | //! # Features 7 | //! - This crate uses [biscotti](https://docs.rs/biscotti/latest/biscotti/) under the hood, which inherit most of it's features. 8 | //! - Offers the ability to configure settings on a per cookie basis. 9 | //! - Enforces type definitions for deserializing cookie values upon retrieval. 10 | //! - Allows customization of both the data type and data serialization. 11 | //! - Provides a straightforward and type safe interface for managing cookies. 12 | //! 13 | //! # Usage 14 | //! To start using the cookiebox in your web application, you must register [CookieMiddleware] in your App. 15 | //!```no_run 16 | //!use actix_web::{web, App, HttpServer, HttpResponse, Error}; 17 | //!use cookiebox::{Processor, ProcessorConfig, CookieMiddleware}; 18 | //! 19 | //!#[actix_web::main] 20 | //!async fn main() -> std::io::Result<()> { 21 | //! // Start by creating a `Processor` from the `ProcessorConfig` 22 | //! // This decides which cookie needs to decrypted or verified. 23 | //! let processor: Processor = ProcessorConfig::default().into(); 24 | //! 25 | //! HttpServer::new(move || 26 | //! App::new() 27 | //! // Add cookie middleware 28 | //! .wrap(CookieMiddleware::new(processor.clone())) 29 | //! .default_service(web::to(|| HttpResponse::Ok()))) 30 | //! .bind(("127.0.0.1", 8080))? 31 | //! .run() 32 | //! .await 33 | //!} 34 | //!``` 35 | //! Define your desired cookie types with customizable configurations. 36 | //!```no_run 37 | //!use actix_web::HttpMessage; 38 | //!use cookiebox::cookiebox_macros::{cookie, FromRequest}; 39 | //!use cookiebox::cookies::{Cookie, CookieName, IncomingConfig, OutgoingConfig}; 40 | //!use cookiebox::{Attributes, SameSite}; 41 | //!use serde_json::json; 42 | //! 43 | //!// Define a cookie type 44 | //!#[cookie(name = "__my-cookie")] 45 | //!pub struct MyCookie; 46 | //! 47 | //!// IncomingConfig gives the cookie type get and get_all cookies with similar name 48 | //!// You may opt out if don't want read cookie data 49 | //!impl IncomingConfig for MyCookie { 50 | //! // Configure the get return to any custom type 51 | //! type Get = String; 52 | //!} 53 | //!// OutgoingConfig gives the cookie type insert and remove cookies 54 | //!// You may opt out if don't want insert or remove a cookie 55 | //!impl OutgoingConfig for MyCookie { 56 | //! // Configure the insert to any custom type 57 | //! type Insert = (String, i32); 58 | //! 59 | //! // In most cases, the default serialization should be sufficient. However, if needed, 60 | //! // you can customize the way the cookie value is serialized by implementing this method. 61 | //! fn serialize(values: Self::Insert) -> serde_json::Value { 62 | //! json!( 63 | //! format!("String: {} - i32: {}", values.0, values.1) 64 | //! ) 65 | //! } 66 | //! 67 | //! // Set the appropriate attribute for the cookie, check `Attributes` for more details 68 | //! fn attributes<'c>() -> Attributes<'c> { 69 | //! Attributes::new().same_site(SameSite::Lax).http_only(false) 70 | //! } 71 | //!} 72 | //!// Once defined, you need to add these cookies in a collection struct and use derive macro to implement FromRequest 73 | //!// Note: The macro only allows struct with either a single unnamed field or multiple named fields 74 | //!#[derive(FromRequest)] 75 | //!pub struct CookieCollection<'c>(Cookie<'c, MyCookie>); 76 | //! 77 | //!``` 78 | //! Now, your cookies can be accessed in request handlers by using `CookieCollection` as a parameter. 79 | //! 80 | //! If you would like to see an example, click [here](https://github.com/MSalah73/cookiebox/tree/master/examples). 81 | 82 | mod attributes; 83 | pub mod cookies; 84 | mod middleware; 85 | mod storage; 86 | 87 | pub use attributes::Attributes; 88 | pub use biscotti::{Expiration, Key, Processor, ProcessorConfig, SameSite, config, time}; 89 | pub use cookiebox_macros; 90 | pub use middleware::CookieMiddleware; 91 | pub use storage::Storage; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![crates.io](https://img.shields.io/crates/v/cookiebox?label=latest)](https://crates.io/crates/cookiebox) 3 | [![build status](https://github.com/Msalah73/cookiebox/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/Msalah73/cookiebox/actions/workflows/ci.yml/) 4 | [![Documentation](https://docs.rs/cookiebox/badge.svg?version=latest)](https://docs.rs/cookiebox/latest) 5 | ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/cookiebox) 6 | [![dependency status](https://deps.rs/repo/github/Msalah73/cookiebox/status.svg)](https://deps.rs/repo/github/Msalah73/cookiebox) 7 | 8 | # Cookiebox 9 | 10 | 11 | 12 | A type safe cookie management crate for the Actix Web framework. 13 | 14 | Cookiebox provides a type safe and flexible approach to managing cookies in Actix web based applications. 15 | It allows you to define, configure, and manage cookies with minimal boilerplate. 16 | 17 | ## Features 18 | - This crate uses [biscotti](https://docs.rs/biscotti/latest/biscotti/) under the hood, which inherit most of it's features. 19 | - Offers the ability to configure settings on a per cookie basis. 20 | - Enforces type definitions for deserializing cookie values upon retrieval. 21 | - Allows customization of both the data type and data serialization. 22 | - Provides a straightforward and type safe interface for managing cookies. 23 | 24 | ## Usage 25 | To start using the cookiebox in your web application, you must register [CookieMiddleware] in your App. 26 | ```rust 27 | use actix_web::{web, App, HttpServer, HttpResponse, Error}; 28 | use cookiebox::{Processor, ProcessorConfig, CookieMiddleware}; 29 | 30 | #[actix_web::main] 31 | async fn main() -> std::io::Result<()> { 32 | // Start by creating a `Processor` from the `ProcessorConfig` 33 | // This decides which cookie needs to decrypted or verified. 34 | let processor: Processor = ProcessorConfig::default().into(); 35 | 36 | HttpServer::new(move || 37 | App::new() 38 | // Add cookie middleware 39 | .wrap(CookieMiddleware::new(processor.clone())) 40 | .default_service(web::to(|| HttpResponse::Ok()))) 41 | .bind(("127.0.0.1", 8080))? 42 | .run() 43 | .await 44 | } 45 | ``` 46 | Define your desired cookie types with customizable configurations. 47 | ```rust 48 | use actix_web::HttpMessage; 49 | use cookiebox::cookiebox_macros::{cookie, FromRequest}; 50 | use cookiebox::cookies::{Cookie, CookieName, IncomingConfig, OutgoingConfig}; 51 | use cookiebox::{Attributes, SameSite}; 52 | use serde_json::json; 53 | 54 | // Define a cookie type 55 | #[cookie(name = "__my-cookie")] 56 | pub struct MyCookie; 57 | 58 | // IncomingConfig gives the cookie type get and get_all cookies with similar name 59 | // You may opt out if don't want read cookie data 60 | impl IncomingConfig for MyCookie { 61 | // Configure the get return to any custom type 62 | type Get = String; 63 | } 64 | // OutgoingConfig gives the cookie type insert and remove cookies 65 | // You may opt out if don't want insert or remove a cookie 66 | impl OutgoingConfig for MyCookie { 67 | // Configure the insert to any custom type 68 | type Insert = (String, i32); 69 | 70 | // In most cases, the default serialization should be sufficient. However, if needed, 71 | // you can customize the way the cookie value is serialized by implementing this method. 72 | fn serialize(values: Self::Insert) -> serde_json::Value { 73 | json!( 74 | format!("String: {} - i32: {}", values.0, values.1) 75 | ) 76 | } 77 | 78 | // Set the appropriate attribute for the cookie, check `Attributes` for more details 79 | fn attributes<'c>() -> Attributes<'c> { 80 | Attributes::new().same_site(SameSite::Lax).http_only(false) 81 | } 82 | } 83 | // Once defined, you need to add these cookies in a collection struct and use derive macro to implement FromRequest 84 | // Note: The macro only allows struct with either a single unnamed field or multiple named fields 85 | #[derive(FromRequest)] 86 | pub struct CookieCollection<'c>(Cookie<'c, MyCookie>); 87 | 88 | ``` 89 | Now, your cookies can be accessed in request handlers by using `CookieCollection` as a parameter. 90 | 91 | If you would like to see an example, click [here](https://github.com/MSalah73/cookiebox/tree/master/examples). 92 | 93 | 94 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{ 6 | parse_macro_input, DeriveInput, Expr, Fields, ItemStruct, Lit, Meta, PathArguments, Type, 7 | }; 8 | 9 | /// Implements a CookieName trait using passed in name from the macro attribute 10 | #[proc_macro_attribute] 11 | pub fn cookie(attr: TokenStream, item: TokenStream) -> TokenStream { 12 | let input = parse_macro_input!(item as ItemStruct); 13 | 14 | let parsed_attr = parse_macro_input!(attr as Meta); 15 | 16 | let mut cookie_name = String::new(); 17 | 18 | if !parsed_attr.path().is_ident("name") { 19 | return syn::Error::new_spanned( 20 | parsed_attr.path().get_ident(), 21 | "Expected `name` parameter: #[cookie(name = \"...\")]", 22 | ) 23 | .into_compile_error() 24 | .into(); 25 | } 26 | if let Meta::NameValue(nv) = parsed_attr { 27 | if let Expr::Lit(expr) = &nv.value { 28 | if let Lit::Str(lit_str) = &expr.lit { 29 | cookie_name.push_str(&lit_str.value()); 30 | } 31 | } 32 | } 33 | 34 | let cookie_struct = &input.ident; 35 | 36 | let expanded = quote! { 37 | #input 38 | 39 | impl CookieName for #cookie_struct { 40 | const COOKIE_NAME: &'static str = #cookie_name; 41 | } 42 | }; 43 | 44 | expanded.into() 45 | } 46 | 47 | /// Implements a FromRequest for a struct that holds cookie types 48 | /// 49 | /// **Note**: only allows structs with either a single unnamed field or multiple unnamed fields 50 | #[proc_macro_derive(FromRequest)] 51 | pub fn cookie_collection(item: TokenStream) -> TokenStream { 52 | let input = parse_macro_input!(item as DeriveInput); 53 | let collection_struct = &input.ident; 54 | 55 | // Extract the field types based on whether it's a tuple or named struct. 56 | let (field_names, field_types) = match extract_fields_types(&input) { 57 | Ok(fields) => fields, 58 | Err(e) => return e.into_compile_error().into(), 59 | }; 60 | 61 | // Extract the generic type argument from a Cookie<'c, SomeType> type. 62 | let inner_types = field_types 63 | .iter() 64 | .try_fold( 65 | Vec::new(), 66 | |mut types, field_type| match extract_cookie_inner_type(field_type) { 67 | Some(inner_type) => { 68 | types.push(inner_type); 69 | Ok(types) 70 | } 71 | None => Err(syn::Error::new_spanned( 72 | field_type, 73 | "Expected field type to be `Cookie<'c, SomeType>`", 74 | )), 75 | }, 76 | ); 77 | 78 | let inner_types = match inner_types { 79 | Ok(types) => types, 80 | Err(error) => return error.into_compile_error().into(), 81 | }; 82 | 83 | let generated_types = if let Some(field_names) = field_names { 84 | quote! { #collection_struct { #( #field_names: Cookie::<#inner_types>::new(&storage),)* }} 85 | } else { 86 | quote! { #collection_struct ( #( Cookie::<#inner_types>::new(&storage),)* )} 87 | }; 88 | 89 | // Generate the implementation for FromRequest 90 | let expanded = quote! { 91 | impl actix_web::FromRequest for #collection_struct<'static> { 92 | type Error = Box; 93 | type Future = std::future::Ready>; 94 | 95 | fn from_request(req: &actix_web::HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future { 96 | match req.extensions().get::() { 97 | Some(storage) => { 98 | std::future::ready(Ok( #generated_types )) 99 | } 100 | None => std::future::ready(Err("Storage not found in request extension".into())), 101 | } 102 | } 103 | } 104 | }; 105 | 106 | expanded.into() 107 | } 108 | 109 | fn extract_fields_types( 110 | input: &DeriveInput, 111 | ) -> Result<(Option>, Vec<&Type>), syn::Error> { 112 | match &input.data { 113 | syn::Data::Struct(data_struct) => match &data_struct.fields { 114 | Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { 115 | Ok((None, vec![&fields.unnamed[0].ty])) 116 | } 117 | Fields::Named(fields) => { 118 | // Unwrap here is okay since Fields::Named require a field name which make a None ident value impossible to represent 119 | let field_names = fields 120 | .named 121 | .iter() 122 | .map(|f| f.ident.clone().unwrap()) 123 | .collect(); 124 | let field_types = fields.named.iter().map(|f| &f.ty).collect(); 125 | Ok((Some(field_names), field_types)) 126 | } 127 | // Units and unnamed with more than 1 fields 128 | token => Err(syn::Error::new_spanned( 129 | token, 130 | "Expected a single unnamed field or multiple named fields", 131 | )), 132 | }, 133 | // Enum and union 134 | _ => Err(syn::Error::new_spanned(input, "Expected a struct")), 135 | } 136 | } 137 | 138 | /// Extracts the inner type (SomeType) from a `Cookie<'c, SomeType>` type. 139 | fn extract_cookie_inner_type(field_type: &Type) -> Option<&Type> { 140 | if let Type::Path(type_path) = field_type { 141 | let segment = type_path.path.segments.first()?; 142 | if segment.ident == "Cookie" { 143 | if let PathArguments::AngleBracketed(generics) = &segment.arguments { 144 | if generics.args.len() == 2 { 145 | if let syn::GenericArgument::Type(inner_type) = &generics.args[1] { 146 | return Some(inner_type); 147 | } 148 | } 149 | } 150 | } 151 | } 152 | None 153 | } 154 | -------------------------------------------------------------------------------- /examples/cookiebox.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{App, HttpMessage, HttpResponse, HttpServer, get}; 2 | use cookiebox::cookiebox_macros::{FromRequest, cookie}; 3 | use cookiebox::cookies::{Cookie, CookieName, IncomingConfig, OutgoingConfig}; 4 | use cookiebox::{Attributes, SameSite}; 5 | use cookiebox::{ 6 | CookieMiddleware, Key, Processor, ProcessorConfig, 7 | config::{CryptoAlgorithm, CryptoRule}, 8 | }; 9 | use serde::{Deserialize, Serialize}; 10 | use serde_json::json; 11 | 12 | #[actix_web::main] 13 | async fn main() -> std::io::Result<()> { 14 | // Set up the processor for the middleware 15 | let mut cookie_config = ProcessorConfig::default(); 16 | 17 | // Set up the rules encrypted cookies 18 | let crypto_rule = CryptoRule { 19 | cookie_names: vec!["__cookie-b".to_string()], 20 | algorithm: CryptoAlgorithm::Encryption, 21 | key: Key::generate(), 22 | fallbacks: vec![], 23 | }; 24 | 25 | cookie_config.crypto_rules.push(crypto_rule); 26 | 27 | let processor: Processor = cookie_config.into(); 28 | 29 | HttpServer::new(move || { 30 | App::new() 31 | // The middleware handles the extraction and transformation the cookies from the request handler 32 | .wrap(CookieMiddleware::new(processor.clone())) 33 | // Cookie A handlers 34 | .service(get_cookie_a) 35 | .service(add_cookie_a) 36 | .service(update_cookie_a) 37 | .service(remove_cookie_a) 38 | // Cookie B handlers 39 | .service(get_cookie_b) 40 | .service(add_cookie_b) 41 | .service(update_cookie_b) 42 | .service(remove_cookie_b) 43 | }) 44 | .bind(("127.0.0.1", 8080))? 45 | .run() 46 | .await 47 | } 48 | 49 | // Data Types 50 | #[derive(Serialize, Deserialize, Debug)] 51 | pub struct CookieData { 52 | pub data: String, 53 | } 54 | 55 | //Define cookies 56 | #[cookie(name = "__cookie-a")] 57 | pub struct CookieA; 58 | 59 | #[cookie(name = "__cookie-b")] 60 | pub struct CookieB; 61 | 62 | // Cookie type configuration 63 | // 64 | // Cookie A 65 | // This generic type parameter would give Cookie type get, get_all, insert, and remove with default attributes and serialization. 66 | // Check Attribute::default for reference 67 | impl IncomingConfig for CookieA { 68 | type Get = String; 69 | } 70 | impl OutgoingConfig for CookieA { 71 | type Insert = String; 72 | } 73 | // Cookie B 74 | // This generic type parameter would give Cookie type get, get_all, insert, and remove. 75 | impl IncomingConfig for CookieB { 76 | type Get = CookieData; 77 | } 78 | impl OutgoingConfig for CookieB { 79 | type Insert = (String, i32); 80 | 81 | // Customize serialization method 82 | fn serialize(values: Self::Insert) -> serde_json::Value { 83 | json!({ 84 | "data": format!("Name: {} - Age: {}", values.0, values.1) 85 | }) 86 | } 87 | // Configure attributes for cookie 88 | fn attributes<'c>() -> Attributes<'c> { 89 | Attributes::new().same_site(SameSite::Lax).http_only(true) 90 | } 91 | } 92 | // Implement FromRequest for CookieCollection 93 | #[derive(FromRequest)] 94 | pub struct CookieCollection<'c> { 95 | cookie_a: Cookie<'c, CookieA>, 96 | cookie_b: Cookie<'c, CookieB>, 97 | } 98 | 99 | #[get("add_cookie_b")] 100 | async fn add_cookie_b(cookies_collection: CookieCollection<'_>) -> HttpResponse { 101 | cookies_collection 102 | .cookie_b 103 | .insert(("Scarlet".to_string(), 27)); 104 | 105 | HttpResponse::Ok().body("Encrypted cookie added") 106 | } 107 | #[get("get_cookie_b")] 108 | async fn get_cookie_b(cookies_collection: CookieCollection<'_>) -> HttpResponse { 109 | // This returns a Ok(CookieData) if found, otherwise Err(CookieBoxError) 110 | let data = cookies_collection 111 | .cookie_b 112 | .get() 113 | .map_err(|e| eprint!("Unable to get cookie data - {e}")); 114 | 115 | HttpResponse::Ok().json(data) 116 | } 117 | 118 | #[get("update_cookie_b")] 119 | async fn update_cookie_b(cookies_collection: CookieCollection<'_>) -> HttpResponse { 120 | // This returns a Ok(CookieData) if found, otherwise Err(CookieBoxError) 121 | let old_data = cookies_collection 122 | .cookie_b 123 | .get() 124 | .map_err(|e| eprint!("Unable to get cookie data - {e}")); 125 | 126 | // Since the path, domain, and name are the same, this would replace the current data with the below 127 | cookies_collection 128 | .cookie_b 129 | .insert(("Jason".to_string(), 22)); 130 | 131 | HttpResponse::Ok().body(format!( 132 | "old data: {:?} - Go to get_cookie_b to check the new value", 133 | old_data 134 | )) 135 | } 136 | 137 | #[get("remove_cookie_b")] 138 | async fn remove_cookie_b(cookies_collection: CookieCollection<'_>) -> HttpResponse { 139 | cookies_collection.cookie_b.remove(); 140 | 141 | HttpResponse::Ok().body("__cookie-b removed") 142 | } 143 | 144 | //Add a new cookie in the browser with the value `%22STRING%22` and set the attributes to default values to get 145 | #[get("add_cookie_a")] 146 | async fn add_cookie_a(cookies_collection: CookieCollection<'_>) -> HttpResponse { 147 | cookies_collection.cookie_a.insert("Cookie A".to_string()); 148 | 149 | HttpResponse::Ok().body("__cookie-a added") 150 | } 151 | // Add a new cookie in the browser with the value `%22STRING%22` and set the attributes to default values to get 152 | #[get("get_cookie_a")] 153 | async fn get_cookie_a(cookies_collection: CookieCollection<'_>) -> HttpResponse { 154 | // This returns a Ok(String) if found, otherwise Err(CookieBoxError) 155 | let data = cookies_collection 156 | .cookie_a 157 | .get() 158 | .map_err(|e| eprint!("Unable to get cookie data - {e}")); 159 | 160 | HttpResponse::Ok().body(format!("{:?}", data)) 161 | } 162 | 163 | #[get("update_cookie_a")] 164 | async fn update_cookie_a(cookies_collection: CookieCollection<'_>) -> HttpResponse { 165 | // This returns a Ok(CookieData) if found, otherwise Err(CookieBoxError) 166 | let old_data = cookies_collection 167 | .cookie_a 168 | .get() 169 | .map_err(|e| eprint!("Unable to get cookie data - {e}")); 170 | 171 | // Since the path, domain, and name are the same, this would replace the current data with the below 172 | cookies_collection 173 | .cookie_a 174 | .insert("New cookie A value".to_string()); 175 | 176 | HttpResponse::Ok().body(format!( 177 | "old data: {:?} - Go to get_cookie_a to check the new value", 178 | old_data 179 | )) 180 | } 181 | 182 | #[get("remove_cookie_a")] 183 | async fn remove_cookie_a(cookies_collection: CookieCollection<'_>) -> HttpResponse { 184 | cookies_collection.cookie_a.remove(); 185 | 186 | HttpResponse::Ok().body("__cookie-a removed") 187 | } 188 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | use actix_utils::future::{Ready, ready}; 2 | use actix_web::{ 3 | HttpMessage, HttpResponse, 4 | dev::{ResponseHead, Service, ServiceRequest, ServiceResponse, Transform, forward_ready}, 5 | http::header::{HeaderValue, SET_COOKIE}, 6 | }; 7 | use anyhow::anyhow; 8 | use biscotti::{Processor, RequestCookie, errors::ProcessIncomingError}; 9 | use std::{future::Future, pin::Pin, rc::Rc}; 10 | 11 | use crate::Storage; 12 | 13 | /// cookiebox's cookie middleware 14 | /// 15 | /// [CookieMiddleware] generates storage data from the cookie header and transform cookies via the [Processor](https://docs.rs/biscotti/latest/biscotti/struct.Processor.html). 16 | /// 17 | /// ```no_run 18 | /// use actix_web::{web, App, HttpServer, HttpResponse}; 19 | /// use cookiebox::{Processor, ProcessorConfig, CookieMiddleware}; 20 | /// 21 | /// #[actix_web::main] 22 | /// async fn main() -> std::io::Result<()> { 23 | /// // Start by creating a `Processor` from the `ProcessorConfig` 24 | /// // This decides which cookie needs to decrypted or verified. 25 | /// let processor: Processor = ProcessorConfig::default().into(); 26 | /// 27 | /// HttpServer::new(move || 28 | /// App::new() 29 | /// // Add cookie middleware 30 | /// .wrap(CookieMiddleware::new(processor.clone())) 31 | /// .default_service(web::to(|| HttpResponse::Ok()))) 32 | /// .bind(("127.0.0.1", 8080))? 33 | /// .run() 34 | /// .await 35 | /// } 36 | /// ``` 37 | pub struct CookieMiddleware { 38 | processor: Rc, 39 | } 40 | 41 | impl CookieMiddleware { 42 | pub fn new(processor: Processor) -> Self { 43 | Self { 44 | processor: Rc::new(processor), 45 | } 46 | } 47 | } 48 | 49 | impl Transform for CookieMiddleware 50 | where 51 | S: Service, Error = actix_web::Error> + 'static, 52 | B: 'static, 53 | { 54 | type Response = ServiceResponse; 55 | type Error = actix_web::Error; 56 | type InitError = (); 57 | type Transform = InnerCookieMiddleware; 58 | type Future = Ready>; 59 | 60 | fn new_transform(&self, service: S) -> Self::Future { 61 | ready(Ok(InnerCookieMiddleware { 62 | service: Rc::new(service), 63 | processor: Rc::clone(&self.processor), 64 | })) 65 | } 66 | } 67 | 68 | pub fn e500(e: T) -> actix_web::Error 69 | where 70 | T: std::fmt::Debug + std::fmt::Display + 'static, 71 | { 72 | actix_web::error::InternalError::from_response(e, HttpResponse::InternalServerError().finish()) 73 | .into() 74 | } 75 | 76 | pub struct InnerCookieMiddleware { 77 | service: Rc, 78 | processor: Rc, 79 | } 80 | 81 | impl Service for InnerCookieMiddleware 82 | where 83 | S: Service, Error = actix_web::Error> + 'static, 84 | S::Future: 'static, 85 | { 86 | type Response = ServiceResponse; 87 | type Error = actix_web::Error; 88 | type Future = Pin>>>; 89 | 90 | forward_ready!(service); 91 | 92 | fn call(&self, req: ServiceRequest) -> Self::Future { 93 | let service = Rc::clone(&self.service); 94 | let processor = Rc::clone(&self.processor); 95 | let storage = Storage::new(); 96 | 97 | Box::pin(async move { 98 | extract_cookies(&req, &processor, storage.clone()).map_err(e500)?; 99 | 100 | req.extensions_mut().insert(storage.clone()); 101 | 102 | let mut response = service.call(req).await?; 103 | 104 | process_response_cookies( 105 | response.response_mut().head_mut(), 106 | &processor, 107 | storage.clone(), 108 | ) 109 | .map_err(e500)?; 110 | 111 | Ok(response) 112 | }) 113 | } 114 | } 115 | 116 | // Currently, the parse header methods and process_incoming method does not support returning a RequestCookie with owned 117 | // name and value only borrowed. for the time being, I have reconstructed the parse header method to do just that until proper 118 | // support in added to the biscotti crate. 119 | /// Extract the cookies from the cookie header and fill the storage with incoming cookie 120 | fn extract_cookies( 121 | req: &ServiceRequest, 122 | processor: &Processor, 123 | storage: Storage, 124 | ) -> Result<(), anyhow::Error> { 125 | let cookie_header = req.headers().get(actix_web::http::header::COOKIE); 126 | 127 | let cookie_header = match cookie_header { 128 | Some(header) => header 129 | .to_str() 130 | .map_err(|e| anyhow!("Invalid cookie header encoding: {}", e))?, 131 | None => return Ok(()), 132 | }; 133 | 134 | for cookie in cookie_header.split(';') { 135 | if cookie.chars().all(char::is_whitespace) { 136 | continue; 137 | } 138 | 139 | let (name, value) = match cookie.split_once('=') { 140 | Some((name, value)) => (name.trim(), value.trim()), 141 | None => { 142 | return Err(anyhow!( 143 | "Expected a name-value pair, but no `=` was found in `{}`", 144 | cookie.to_string() 145 | )); 146 | } 147 | }; 148 | 149 | if name.is_empty() { 150 | return Err(anyhow!( 151 | "The name of a cookie cannot be empty, but found an empty name with `{}` as value", 152 | value.to_string() 153 | )); 154 | } 155 | 156 | let cookie = match processor.process_incoming(name, value) { 157 | Ok(c) => c, 158 | Err(e) => { 159 | let t = match e { 160 | ProcessIncomingError::Crypto(_) => "an encrypted", 161 | ProcessIncomingError::Decoding(_) => "a singed", 162 | _ => "an unknown", 163 | }; 164 | return Err(anyhow!( 165 | "Failed to process `{}` as {t} request cookie", 166 | name 167 | )); 168 | } 169 | }; 170 | 171 | let cookie = RequestCookie::new(cookie.name().to_owned(), cookie.value().to_owned()); 172 | storage.request_storage.borrow_mut().append(cookie); 173 | } 174 | 175 | Ok(()) 176 | } 177 | /// Encrypt or singed outgoing cookie before sending it off 178 | fn process_response_cookies( 179 | response: &mut ResponseHead, 180 | processor: &Processor, 181 | storage: Storage, 182 | ) -> Result<(), anyhow::Error> { 183 | let response_storage = storage.response_storage.take(); 184 | for cookie in response_storage.header_values(processor) { 185 | let cookie = HeaderValue::from_str(&cookie) 186 | .map_err(|e| anyhow!("Failed to attached cookies to outgoing response: {}", e))?; 187 | response.headers_mut().append(SET_COOKIE, cookie); 188 | } 189 | 190 | Ok(()) 191 | } 192 | -------------------------------------------------------------------------------- /src/attributes.rs: -------------------------------------------------------------------------------- 1 | use biscotti::{Expiration, time::SignedDuration}; 2 | use biscotti::{RemovalCookie, ResponseCookie, ResponseCookieId, SameSite}; 3 | use std::borrow::Cow; 4 | 5 | /// Simple builder for cookie attributes 6 | /// 7 | /// [Attributes] acts as a facade to [ResponseCookie](https://docs.rs/biscotti/latest/biscotti/struct.ResponseCookie.html) and [RemovalCoolie](https://docs.rs/biscotti/latest/biscotti/struct.RemovalCookie.html) 8 | /// 9 | /// ```no_run 10 | /// use cookiebox::cookiebox_macros::cookie; 11 | /// use cookiebox::cookies::{CookieName, OutgoingConfig}; 12 | /// use cookiebox::{Attributes, SameSite, Expiration}; 13 | /// use cookiebox::time::{SignedDuration, civil::date,tz::TimeZone}; 14 | /// 15 | /// #[cookie(name = "my-cookie")] 16 | /// pub struct MyCookie; 17 | /// 18 | /// impl OutgoingConfig for MyCookie { 19 | /// type Insert = String; 20 | /// 21 | /// fn attributes<'c>() -> Attributes<'c> { 22 | /// 23 | /// let date = date(2024, 1, 15) 24 | /// .at(0,0,0, 0) 25 | /// .to_zoned(TimeZone::UTC) 26 | /// .unwrap(); 27 | /// 28 | /// Attributes::new() 29 | /// .path("/some-path") 30 | /// // the leading dot is stripped 31 | /// .domain("..example.com") 32 | /// .same_site(SameSite::Lax) 33 | /// .secure(true) 34 | /// .http_only(true) 35 | /// .partitioned(true) 36 | /// .expires(Expiration::from(date)) 37 | /// // max_age take precedence over expires 38 | /// .max_age(SignedDuration::from_hours(10)) 39 | /// // This sets max_age and expires to 20 years in the future 40 | /// .permanent(true) 41 | /// } 42 | /// } 43 | /// ``` 44 | pub struct Attributes<'c> { 45 | path: Option>, 46 | domain: Option>, 47 | secure: Option, 48 | http_only: Option, 49 | partitioned: Option, 50 | same_site: Option, 51 | max_age: Option, 52 | expires: Option, 53 | permanent: bool, 54 | } 55 | impl<'c> Attributes<'c> { 56 | /// Create a new [Attributes] instance 57 | pub fn new() -> Self { 58 | Attributes { 59 | path: None, 60 | http_only: None, 61 | same_site: None, 62 | domain: None, 63 | secure: None, 64 | partitioned: None, 65 | max_age: None, 66 | expires: None, 67 | permanent: false, 68 | } 69 | } 70 | /// Sets the `path` of `self` to `path` 71 | #[inline] 72 | pub fn path>>(mut self, path: T) -> Self { 73 | self.path = Some(path.into()); 74 | self 75 | } 76 | /// Sets the `domain` of `self` to `domain` 77 | /// 78 | /// **Note**: if the Domain starts with a leading `.`, the leading `.` is stripped. 79 | #[inline] 80 | pub fn domain>>(mut self, domain: T) -> Self { 81 | self.domain = Some(domain.into()); 82 | self 83 | } 84 | /// Sets the `secure` of `self` to `value` 85 | #[inline] 86 | pub fn secure>>(mut self, value: T) -> Self { 87 | self.secure = value.into(); 88 | self 89 | } 90 | /// Sets the `http_only` of `self` to `value` 91 | #[inline] 92 | pub fn http_only>>(mut self, value: T) -> Self { 93 | self.http_only = value.into(); 94 | self 95 | } 96 | /// Sets the `same_site` of `self` to `value` 97 | /// 98 | /// **Note**: If `SameSite` attribute is set to `None`, the `Secure` flag will be set automatically , unless explicitly set to `false`. 99 | pub fn same_site>>(mut self, value: T) -> Self { 100 | self.same_site = value.into(); 101 | self 102 | } 103 | /// Sets the `max_age` of `self` to `value` 104 | #[inline] 105 | pub fn max_age>>(mut self, value: T) -> Self { 106 | self.max_age = value.into(); 107 | self 108 | } 109 | /// Sets the `expires` of `self` to `value` 110 | #[inline] 111 | pub fn expires>(mut self, value: T) -> Self { 112 | self.expires = Some(value.into()); 113 | self 114 | } 115 | /// Sets the `partitioned` of `self` to `value` 116 | /// 117 | /// **Note**: Partitioned cookies require the `Secure` attribute. If not set explicitly, the browser will automatically set it to `true`. 118 | #[inline] 119 | pub fn partitioned>>(mut self, value: T) -> Self { 120 | self.partitioned = value.into(); 121 | self 122 | } 123 | /// Sets the `permanent` of `self` to `value` 124 | #[inline] 125 | pub fn permanent(mut self, value: bool) -> Self { 126 | self.permanent = value; 127 | self 128 | } 129 | } 130 | /// Create [Attributes] with default values - `path: "/"`, `SameSite: Lax`, and `http_only: true` 131 | impl Default for Attributes<'_> { 132 | fn default() -> Self { 133 | Attributes { 134 | path: Some("/".into()), 135 | http_only: Some(true), 136 | same_site: Some(SameSite::Lax), 137 | domain: None, 138 | secure: None, 139 | partitioned: None, 140 | max_age: None, 141 | expires: None, 142 | permanent: false, 143 | } 144 | } 145 | } 146 | 147 | pub(crate) trait AttributesSetter<'c> { 148 | fn set_attributes(self, attributes: &Attributes<'c>) -> Self; 149 | } 150 | 151 | impl<'c> AttributesSetter<'c> for ResponseCookie<'c> { 152 | fn set_attributes(mut self, attributes: &Attributes<'c>) -> Self { 153 | if let Some(path) = &attributes.path { 154 | self = self.set_path(path.clone()) 155 | } 156 | if let Some(domain) = &attributes.domain { 157 | self = self.set_domain(domain.clone()) 158 | } 159 | 160 | if attributes.permanent { 161 | self = self.make_permanent() 162 | } else { 163 | self = self.set_max_age(attributes.max_age); 164 | 165 | if let Some(expires) = &attributes.expires { 166 | self = self.set_expires(expires.clone()) 167 | } 168 | } 169 | 170 | self.set_secure(attributes.secure) 171 | .set_http_only(attributes.http_only) 172 | .set_same_site(attributes.same_site) 173 | .set_partitioned(attributes.partitioned) 174 | } 175 | } 176 | 177 | impl<'c> AttributesSetter<'c> for RemovalCookie<'c> { 178 | fn set_attributes(mut self, attributes: &Attributes<'c>) -> Self { 179 | if let Some(path) = &attributes.path { 180 | self = self.set_path(path.clone()) 181 | } 182 | if let Some(domain) = &attributes.domain { 183 | self = self.set_domain(domain.clone()) 184 | } 185 | self 186 | } 187 | } 188 | 189 | impl<'c> AttributesSetter<'c> for ResponseCookieId<'c> { 190 | fn set_attributes(mut self, attributes: &Attributes<'c>) -> Self { 191 | if let Some(path) = &attributes.path { 192 | self = self.set_path(path.clone()) 193 | } 194 | if let Some(domain) = &attributes.domain { 195 | self = self.set_domain(domain.clone()) 196 | } 197 | self 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /macros/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /src/cookies.rs: -------------------------------------------------------------------------------- 1 | //! cookiebox's core functionality 2 | use crate::attributes::{Attributes, AttributesSetter}; 3 | use crate::storage::Storage; 4 | use biscotti::{RemovalCookie, ResponseCookie, ResponseCookieId}; 5 | use serde::Serialize; 6 | use serde::de::DeserializeOwned; 7 | use serde_json::{Value, json}; 8 | use std::any::type_name; 9 | use thiserror::Error; 10 | 11 | /// The error returned by [IncomingConfig] get methods 12 | #[derive(Error, Debug, PartialEq)] 13 | pub enum CookieBoxError { 14 | #[error("`{0}` does not exist")] 15 | NotFound(String), 16 | #[error("Failed to deserialize `{0}` to type `{1}`")] 17 | Deserialization(String, String), 18 | } 19 | 20 | /// Base struct for cookie generic types 21 | pub struct Cookie<'c, T> { 22 | storage: Storage<'c>, 23 | attributes: Option>, 24 | _marker: std::marker::PhantomData, 25 | } 26 | 27 | impl<'c, T> Cookie<'c, T> { 28 | /// Create a cookie instance for any generic type parameter 29 | pub fn new(storage: &Storage<'c>) -> Self { 30 | Cookie { 31 | storage: storage.clone(), 32 | attributes: None, 33 | _marker: std::marker::PhantomData, 34 | } 35 | } 36 | } 37 | /// Provide methods to `get` data from a cookie instance for any generic type parameter that implements [IncomingConfig] 38 | impl Cookie<'_, T> { 39 | /// Retrieves the data from the [Storage] request collection using the cookie name specified by [CookieName]. 40 | /// 41 | /// The deserialized date is returned as the associated type defined by the `Get` type from [IncomingConfig]. 42 | /// # Example 43 | /// ```no_run 44 | /// use cookiebox::cookiebox_macros::{cookie, FromRequest}; 45 | /// use cookiebox::cookies::{Cookie, CookieName, IncomingConfig}; 46 | /// use actix_web::{HttpResponse, HttpMessage}; 47 | /// 48 | /// // Set up a generic cookie type 49 | /// #[cookie(name = "my-cookie")] 50 | /// pub struct MyCookie; 51 | /// 52 | /// impl IncomingConfig for MyCookie { 53 | /// type Get = String; 54 | /// } 55 | /// 56 | /// // Use macro to implement `FromRequest` for cookie collection struct 57 | /// #[derive(FromRequest)] 58 | /// pub struct CookieCollection<'c>(Cookie<'c, MyCookie>); 59 | /// 60 | /// async fn get_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 61 | /// cookie.0.get(); 62 | /// HttpResponse::Ok().finish() 63 | /// } 64 | /// ``` 65 | pub fn get(&self) -> Result { 66 | let data = &self 67 | .storage 68 | .request_storage 69 | .borrow() 70 | .get(T::COOKIE_NAME) 71 | .ok_or(CookieBoxError::NotFound(T::COOKIE_NAME.to_string()))?; 72 | 73 | let data = serde_json::from_str(data.value()).map_err(|_| { 74 | CookieBoxError::Deserialization( 75 | data.value().to_string(), 76 | type_name::().to_string(), 77 | ) 78 | })?; 79 | Ok(data) 80 | } 81 | 82 | /// Retrieves a list of data items from the [Storage] request collection with the same name using the cookie name specified by [CookieName]. 83 | /// 84 | /// Each item in the list is of the associated type `Get` from the [IncomingConfig]. 85 | /// 86 | /// # Example 87 | /// ```no_run 88 | /// use cookiebox::cookiebox_macros::{cookie, FromRequest}; 89 | /// use cookiebox::cookies::{Cookie, CookieName, IncomingConfig}; 90 | /// use actix_web::{HttpResponse, HttpMessage}; 91 | /// 92 | /// // Set up generic cookie type 93 | /// #[cookie(name = "my-cookie")] 94 | /// pub struct MyCookie; 95 | /// 96 | /// impl IncomingConfig for MyCookie { 97 | /// type Get = String; 98 | /// } 99 | /// 100 | /// // Use macro to implement `FromRequest` for cookie collection struct 101 | /// #[derive(FromRequest)] 102 | /// pub struct CookieCollection<'c>(Cookie<'c, MyCookie>); 103 | /// 104 | /// async fn get_all_cookies(cookie: CookieCollection<'_>) -> HttpResponse { 105 | /// // return a Vec of set type 106 | /// cookie.0.get_all(); 107 | /// HttpResponse::Ok().finish() 108 | /// } 109 | /// ``` 110 | pub fn get_all(&self) -> Result, CookieBoxError> { 111 | let data = &self.storage.request_storage.borrow(); 112 | 113 | let data = data 114 | .get_all(T::COOKIE_NAME) 115 | .ok_or(CookieBoxError::NotFound(T::COOKIE_NAME.to_string()))?; 116 | 117 | let mut result = Vec::new(); 118 | 119 | for value in data.values() { 120 | let data = serde_json::from_str(value).map_err(|_| { 121 | CookieBoxError::Deserialization( 122 | value.to_string(), 123 | type_name::().to_string(), 124 | ) 125 | })?; 126 | result.push(data); 127 | } 128 | 129 | Ok(result) 130 | } 131 | } 132 | 133 | /// Provide methods to `insert` and `remove` a cookie instance for any generic type parameter that implements [OutgoingConfig] 134 | impl Cookie<'_, T> { 135 | /// Add a cookie to the [Storage] response collection which later attached to the HTTP response using the `Set-Cookie` header. 136 | /// 137 | /// # Example 138 | /// ```no_run 139 | /// use cookiebox::cookiebox_macros::{cookie, FromRequest}; 140 | /// use cookiebox::cookies::{Cookie, CookieName, OutgoingConfig}; 141 | /// use actix_web::{HttpResponse, HttpMessage}; 142 | /// 143 | /// // Set up generic cookie type 144 | /// #[cookie(name = "my-cookie")] 145 | /// pub struct MyCookie; 146 | /// 147 | /// impl OutgoingConfig for MyCookie { 148 | /// type Insert = String; 149 | /// } 150 | /// 151 | /// // Use macro to implement `FromRequest` for cookie collection struct 152 | /// #[derive(FromRequest)] 153 | /// pub struct CookieCollection<'c>(Cookie<'c, MyCookie>); 154 | /// 155 | /// async fn insert_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 156 | /// cookie.0.insert("cookie value".to_string()); 157 | /// HttpResponse::Ok().finish() 158 | /// } 159 | /// ``` 160 | pub fn insert(&self, value: T::Insert) { 161 | let data = T::serialize(value); 162 | 163 | let response_cookie = ResponseCookie::new(T::COOKIE_NAME, data.to_string()); 164 | 165 | let attributes = match &self.attributes { 166 | Some(attributes) => attributes, 167 | None => &T::attributes(), 168 | }; 169 | 170 | let response_cookie = response_cookie.set_attributes(attributes); 171 | 172 | self.storage 173 | .response_storage 174 | .borrow_mut() 175 | .insert(response_cookie); 176 | } 177 | /// Add a removal cookie to the [Storage] response collection, which later attached to the HTTP response using the `Set-Cookie` header. 178 | /// 179 | /// Cookie removal is determined by name, path, and domain 180 | /// 181 | /// # Example 182 | /// ```no_run 183 | /// use cookiebox::cookiebox_macros::{cookie, FromRequest}; 184 | /// use cookiebox::cookies::{Cookie, CookieName, OutgoingConfig}; 185 | /// use actix_web::{HttpResponse, HttpMessage}; 186 | /// 187 | /// // Set up generic cookie type 188 | /// #[cookie(name = "my-cookie")] 189 | /// pub struct MyCookie; 190 | /// 191 | /// impl OutgoingConfig for MyCookie { 192 | /// type Insert = String; 193 | /// } 194 | /// 195 | /// // Use macro to implement `FromRequest` for cookie collection struct 196 | /// #[derive(FromRequest)] 197 | /// pub struct CookieCollection<'c>(Cookie<'c, MyCookie>); 198 | /// 199 | /// async fn remove_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 200 | /// cookie.0.remove(); 201 | /// HttpResponse::Ok().finish() 202 | /// } 203 | /// ``` 204 | pub fn remove(&self) { 205 | let attributes = match &self.attributes { 206 | Some(attributes) => attributes, 207 | None => &T::attributes(), 208 | }; 209 | 210 | let removal_cookie = RemovalCookie::new(T::COOKIE_NAME); 211 | 212 | // Sets the domain and path only 213 | let removal_cookie = removal_cookie.set_attributes(attributes); 214 | 215 | // Inserting the removal cookie will replace any cookie with the same name, path, and domain 216 | self.storage 217 | .response_storage 218 | .borrow_mut() 219 | .insert(removal_cookie); 220 | } 221 | /// Discard a cookie from the response collection [Storage] only 222 | /// 223 | /// Discarding a cookie is determined by name, path, and domain 224 | /// 225 | /// # Example 226 | /// ```no_run 227 | /// use cookiebox::cookiebox_macros::{cookie, FromRequest}; 228 | /// use cookiebox::cookies::{Cookie, CookieName, OutgoingConfig}; 229 | /// use actix_web::{HttpResponse, HttpMessage}; 230 | /// 231 | /// // Set up generic cookie type 232 | /// #[cookie(name = "my-cookie")] 233 | /// pub struct MyCookie; 234 | /// 235 | /// impl OutgoingConfig for MyCookie { 236 | /// type Insert = String; 237 | /// } 238 | /// 239 | /// // Use macro to implement `FromRequest` for cookie collection struct 240 | /// #[derive(FromRequest)] 241 | /// pub struct CookieCollection<'c>(Cookie<'c, MyCookie>); 242 | /// 243 | /// async fn discard_cookie(cookie: CookieCollection<'_>) -> HttpResponse { 244 | /// cookie.0.insert("Stephanie".to_string()); 245 | /// cookie.0.discard(); 246 | /// HttpResponse::Ok().finish() 247 | /// } 248 | /// ``` 249 | pub fn discard(&self) { 250 | let discard_id = ResponseCookieId::new(T::COOKIE_NAME); 251 | 252 | let attributes = match &self.attributes { 253 | Some(attributes) => attributes, 254 | None => &T::attributes(), 255 | }; 256 | 257 | // This sets the path and domain only 258 | let discard_id = discard_id.set_attributes(attributes); 259 | 260 | self.storage 261 | .response_storage 262 | .borrow_mut() 263 | .discard(discard_id); 264 | } 265 | } 266 | 267 | /// Provide internal customization for `insert` and `remove` methods in [Cookie]. 268 | /// 269 | /// The `insert` and `remove` will be available when types that implement this trait is used as generic parameters for `Cookie`. 270 | /// ```no_run 271 | /// use cookiebox::cookiebox_macros::cookie; 272 | /// use cookiebox::cookies::{CookieName, OutgoingConfig}; 273 | /// 274 | /// // Define a generic cookie type 275 | /// #[cookie(name = "__my-cookie")] 276 | /// pub struct MyCookie; 277 | /// 278 | /// impl OutgoingConfig for MyCookie { 279 | /// // Configure the insert type 280 | /// type Insert = String; 281 | /// 282 | /// // The default serialization is used here, if customization is needed, implement the `serialize` method. 283 | /// 284 | /// // The default attributes is used here which consists of http-only: true, SameSite: Lax, and 285 | /// // path: "/" 286 | /// } 287 | /// ``` 288 | pub trait OutgoingConfig: CookieName { 289 | /// The serialization type when inserting a cookie to storage 290 | type Insert: Serialize; 291 | 292 | /// Provides default serialization for a cookie. This can be overwriting 293 | fn serialize(values: Self::Insert) -> Value { 294 | json!(values) 295 | } 296 | 297 | /// Provides preset attributes for a cookie. This can be overwriting 298 | fn attributes<'c>() -> Attributes<'c> { 299 | Attributes::default() 300 | } 301 | } 302 | 303 | /// Provide internal customization for `get` and `get_all` methods in [Cookie]. 304 | /// 305 | /// The `get` and `get_all` will be available when types that implement this trait is used as generic parameters for `Cookie`. 306 | /// ```no_run 307 | /// use cookiebox::cookiebox_macros::cookie; 308 | /// use cookiebox::cookies::{CookieName, IncomingConfig}; 309 | /// // Define a generic cookie type struct 310 | /// #[cookie(name = "__my-cookie")] 311 | /// pub struct MyCookie; 312 | /// 313 | /// impl IncomingConfig for MyCookie { 314 | /// // Configure the get return type 315 | /// type Get = String; 316 | /// } 317 | /// ``` 318 | pub trait IncomingConfig: CookieName { 319 | /// The deserialization type when getting a cookie from storage 320 | type Get: DeserializeOwned; 321 | } 322 | 323 | /// This is the base implementation of a cookie type 324 | /// 325 | /// This is either implemented manually or with macro `#[Cookie(name = "...")]` 326 | pub trait CookieName { 327 | const COOKIE_NAME: &'static str; 328 | } 329 | 330 | #[cfg(test)] 331 | mod tests { 332 | use crate::cookiebox_macros::cookie; 333 | use crate::cookies::{Cookie, CookieName, IncomingConfig, OutgoingConfig}; 334 | use crate::time::{SignedDuration, Zoned, civil::date, tz::TimeZone}; 335 | use crate::{Attributes, Expiration, SameSite, Storage}; 336 | use biscotti::{RequestCookie, ResponseCookie}; 337 | use serde::{Deserialize, Serialize}; 338 | use serde_json::json; 339 | 340 | // Cookie types 341 | #[cookie(name = "type_a")] 342 | pub struct TypeA; 343 | #[cookie(name = "type_b")] 344 | pub struct TypeB; 345 | #[cookie(name = "type_c")] 346 | pub struct TypeC; 347 | #[cookie(name = "type_d")] 348 | pub struct TypeD; 349 | 350 | #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] 351 | pub struct GetType { 352 | name: String, 353 | } 354 | 355 | // read and write for type a 356 | impl OutgoingConfig for TypeA { 357 | type Insert = GetType; 358 | } 359 | impl IncomingConfig for TypeA { 360 | type Get = GetType; 361 | } 362 | 363 | // read and write for type b 364 | impl OutgoingConfig for TypeB { 365 | type Insert = (String, i32); 366 | 367 | fn serialize(values: Self::Insert) -> serde_json::Value { 368 | json!({ 369 | "name": format!("{} is {}", values.0, values.1) 370 | }) 371 | } 372 | } 373 | impl IncomingConfig for TypeB { 374 | type Get = GetType; 375 | } 376 | 377 | // read and write for type c 378 | impl OutgoingConfig for TypeC { 379 | type Insert = GetType; 380 | 381 | fn attributes<'c>() -> Attributes<'c> { 382 | // Expiration has an internal From impl for Into 383 | let date = date(2024, 1, 15) 384 | .at(0, 0, 0, 0) 385 | .to_zoned(TimeZone::UTC) 386 | .unwrap(); 387 | 388 | Attributes::new() 389 | .path("/some-path") 390 | .domain("..example.com") 391 | .same_site(SameSite::Lax) 392 | .secure(true) 393 | .http_only(true) 394 | .partitioned(true) 395 | .expires(date) 396 | .max_age(SignedDuration::from_hours(10)) 397 | } 398 | } 399 | impl IncomingConfig for TypeC { 400 | type Get = GetType; 401 | } 402 | 403 | // read and write for type d 404 | impl OutgoingConfig for TypeD { 405 | type Insert = GetType; 406 | 407 | fn attributes<'c>() -> Attributes<'c> { 408 | Attributes::new().permanent(true) 409 | } 410 | } 411 | impl IncomingConfig for TypeD { 412 | type Get = GetType; 413 | } 414 | 415 | #[test] 416 | fn get() { 417 | // Set up 418 | // Initialize storage 419 | let storage = Storage::new(); 420 | let incoming_cookie = RequestCookie::new("type_a", r#"{ "name": "some value" }"#); 421 | let get_type_value = GetType { 422 | name: "some value".to_string(), 423 | }; 424 | 425 | storage.request_storage.borrow_mut().append(incoming_cookie); 426 | 427 | // Use generic type parameter to create a cookie instance 428 | let cookie = Cookie::::new(&storage); 429 | 430 | let typed_request_value = cookie.get(); 431 | 432 | assert_eq!(typed_request_value.is_ok(), true); 433 | assert_eq!(typed_request_value, Ok(get_type_value)); 434 | } 435 | #[test] 436 | fn get_all() { 437 | // Set up 438 | // Initialize storage 439 | let storage = Storage::new(); 440 | let incoming_cookie_a = RequestCookie::new("type_a", r#"{ "name": "some value 1" }"#); 441 | let incoming_cookie_b = RequestCookie::new("type_a", r#"{ "name": "some value 2" }"#); 442 | let get_type_values = vec![ 443 | GetType { 444 | name: "some value 1".to_string(), 445 | }, 446 | GetType { 447 | name: "some value 2".to_string(), 448 | }, 449 | ]; 450 | 451 | storage 452 | .request_storage 453 | .borrow_mut() 454 | .append(incoming_cookie_a); 455 | storage 456 | .request_storage 457 | .borrow_mut() 458 | .append(incoming_cookie_b); 459 | 460 | // Use generic type parameter to create a cookie instance 461 | let cookie = Cookie::::new(&storage); 462 | 463 | let typed_request_value = cookie.get_all(); 464 | 465 | assert_eq!(typed_request_value.is_ok(), true); 466 | assert_eq!(typed_request_value, Ok(get_type_values)); 467 | } 468 | #[test] 469 | fn insert_cookie() { 470 | // Set up 471 | // Initialize storage 472 | let storage = Storage::new(); 473 | let outgoing_cookie = ResponseCookie::new("type_a", r#"{ "name": "some value" }"#); 474 | // The id determined by name path and domain 475 | let outgoing_cookie_id = outgoing_cookie.id().set_path("/"); 476 | let get_type_value = GetType { 477 | name: "some value ".to_string(), 478 | }; 479 | 480 | // Use generic type parameter to create a cookie instance 481 | let cookie = Cookie::::new(&storage); 482 | 483 | cookie.insert(get_type_value); 484 | 485 | let binding = storage.response_storage.borrow(); 486 | let response_cookie = binding.get(outgoing_cookie_id); 487 | 488 | assert_eq!(response_cookie.is_some(), true); 489 | assert_eq!( 490 | response_cookie.unwrap().name_value(), 491 | ("type_a", r#"{"name":"some value "}"#) 492 | ); 493 | } 494 | #[test] 495 | fn insert_cookie_with_custom_serialize_impl() { 496 | // Set up 497 | // Initialize storage 498 | let storage = Storage::new(); 499 | let outgoing_cookie = ResponseCookie::new("type_b", r#"{ "name": "some value is 32" }"#); 500 | // The id determined by name path and domain 501 | let outgoing_cookie_id = outgoing_cookie.id().set_path("/"); 502 | let get_type_value = ("some value".to_string(), 32); 503 | 504 | // Use generic type parameter to create a cookie instance 505 | let cookie = Cookie::::new(&storage); 506 | 507 | cookie.insert(get_type_value); 508 | 509 | let binding = storage.response_storage.borrow(); 510 | let response_cookie = binding.get(outgoing_cookie_id); 511 | 512 | assert_eq!(response_cookie.is_some(), true); 513 | assert_eq!( 514 | response_cookie.unwrap().name_value(), 515 | ("type_b", r#"{"name":"some value is 32"}"#) 516 | ); 517 | } 518 | #[test] 519 | fn insert_cookie_with_custom_attributes() { 520 | // Set up 521 | // Initialize storage 522 | let storage = Storage::new(); 523 | let outgoing_cookie = ResponseCookie::new("type_c", r#"{ "name": "some value" }"#); 524 | // The id determined by name path and domain 525 | let outgoing_cookie_id = outgoing_cookie 526 | .id() 527 | .set_path("/some-path") 528 | .set_domain("..example.com"); 529 | let get_type_value = GetType { 530 | name: "some value".to_string(), 531 | }; 532 | 533 | // Expiration cookie set up 534 | let date = date(2024, 1, 15) 535 | .at(0, 0, 0, 0) 536 | .to_zoned(TimeZone::UTC) 537 | .unwrap(); 538 | 539 | // Use generic type parameter to create a cookie instance 540 | let cookie = Cookie::::new(&storage); 541 | 542 | cookie.insert(get_type_value); 543 | 544 | let binding = storage.response_storage.borrow(); 545 | let response_cookie = binding.get(outgoing_cookie_id); 546 | 547 | assert_eq!(response_cookie.is_some(), true); 548 | assert_eq!( 549 | response_cookie.unwrap().name_value(), 550 | ("type_c", r#"{"name":"some value"}"#) 551 | ); 552 | assert_eq!(response_cookie.unwrap().path(), Some("/some-path")); 553 | assert_eq!(response_cookie.unwrap().domain(), Some(".example.com")); 554 | assert_eq!(response_cookie.unwrap().same_site(), Some(SameSite::Lax)); 555 | assert_eq!(response_cookie.unwrap().http_only(), Some(true)); 556 | assert_eq!(response_cookie.unwrap().secure(), Some(true)); 557 | assert_eq!(response_cookie.unwrap().partitioned(), Some(true)); 558 | assert_eq!( 559 | response_cookie.unwrap().expires(), 560 | Some(&Expiration::from(date)) 561 | ); 562 | assert_eq!( 563 | response_cookie.unwrap().max_age(), 564 | Some(SignedDuration::from_hours(10)) 565 | ); 566 | } 567 | #[test] 568 | fn double_insert_cookie_with_custom_attributes_should_not_change_attributes_values() { 569 | // Set up 570 | // Initialize storage 571 | let storage = Storage::new(); 572 | let outgoing_cookie = ResponseCookie::new("type_c", r#"{ "name": "some value" }"#); 573 | // The id determined by name path and domain 574 | let outgoing_cookie_id = outgoing_cookie 575 | .id() 576 | .set_path("/some-path") 577 | .set_domain("..example.com"); 578 | let get_type_value = GetType { 579 | name: "some value".to_string(), 580 | }; 581 | 582 | // Expiration cookie set up 583 | let date = date(2024, 1, 15) 584 | .at(0, 0, 0, 0) 585 | .to_zoned(TimeZone::UTC) 586 | .unwrap(); 587 | 588 | // Use generic type parameter to create a cookie instance 589 | let cookie = Cookie::::new(&storage); 590 | 591 | cookie.insert(get_type_value.clone()); 592 | cookie.insert(get_type_value); 593 | 594 | let binding = storage.response_storage.borrow(); 595 | let response_cookie = binding.get(outgoing_cookie_id); 596 | 597 | assert_eq!(response_cookie.is_some(), true); 598 | assert_eq!( 599 | response_cookie.unwrap().name_value(), 600 | ("type_c", r#"{"name":"some value"}"#) 601 | ); 602 | assert_eq!(response_cookie.unwrap().path(), Some("/some-path")); 603 | assert_eq!(response_cookie.unwrap().domain(), Some(".example.com")); 604 | assert_eq!(response_cookie.unwrap().same_site(), Some(SameSite::Lax)); 605 | assert_eq!(response_cookie.unwrap().http_only(), Some(true)); 606 | assert_eq!(response_cookie.unwrap().secure(), Some(true)); 607 | assert_eq!(response_cookie.unwrap().partitioned(), Some(true)); 608 | assert_eq!( 609 | response_cookie.unwrap().expires(), 610 | Some(&Expiration::from(date)) 611 | ); 612 | assert_eq!( 613 | response_cookie.unwrap().max_age(), 614 | Some(SignedDuration::from_hours(10)) 615 | ); 616 | } 617 | #[test] 618 | fn insert_cookie_with_permanent() { 619 | // Set up 620 | // Initialize storage 621 | let storage = Storage::new(); 622 | let outgoing_cookie = ResponseCookie::new("type_d", r#"{ "name": "some value" }"#); 623 | // The id determined by name path and domain 624 | let outgoing_cookie_id = outgoing_cookie.id(); 625 | let get_type_value = GetType { 626 | name: "some value".to_string(), 627 | }; 628 | 629 | // Use generic type parameter to create a cookie instance 630 | let cookie = Cookie::::new(&storage); 631 | 632 | cookie.insert(get_type_value); 633 | 634 | let binding = storage.response_storage.borrow(); 635 | let response_cookie = binding.get(outgoing_cookie_id); 636 | 637 | assert_eq!(response_cookie.is_some(), true); 638 | assert_eq!( 639 | response_cookie.unwrap().name_value(), 640 | ("type_d", r#"{"name":"some value"}"#) 641 | ); 642 | assert_eq!( 643 | response_cookie.unwrap().max_age(), 644 | Some(SignedDuration::from_hours(24 * 20 * 365)) 645 | ); 646 | } 647 | #[test] 648 | fn remove_cookie() { 649 | // Set up 650 | // Initialize storage 651 | let storage = Storage::new(); 652 | let outgoing_cookie = ResponseCookie::new("type_b", r#"{ "name": "some value is 32" }"#); 653 | // The id determined by name path and domain 654 | let outgoing_cookie_id = outgoing_cookie.id().set_path("/"); 655 | 656 | // Use generic type parameter to create a cookie instance 657 | let cookie = Cookie::::new(&storage); 658 | 659 | cookie.remove(); 660 | 661 | let binding = storage.response_storage.borrow(); 662 | let response_cookie = binding.get(outgoing_cookie_id); 663 | 664 | assert_eq!(response_cookie.is_some(), true); 665 | assert_eq!(response_cookie.unwrap().name_value(), ("type_b", "")); 666 | assert!( 667 | response_cookie 668 | .unwrap() 669 | .expires() 670 | .unwrap() 671 | .datetime() 672 | .unwrap() 673 | < Zoned::now() 674 | ); 675 | } 676 | #[test] 677 | fn discard_cookie() { 678 | // Set up 679 | // Initialize storage 680 | let storage = Storage::new(); 681 | let outgoing_cookie = ResponseCookie::new("type_b", r#"{ "name": "some value is 32" }"#); 682 | // The id determined by name path and domain 683 | let outgoing_cookie_id = outgoing_cookie.id().set_path("/"); 684 | 685 | // Use generic type parameter to create a cookie instance 686 | let cookie = Cookie::::new(&storage); 687 | 688 | cookie.discard(); 689 | 690 | let binding = storage.response_storage.borrow(); 691 | let response_cookie = binding.get(outgoing_cookie_id); 692 | 693 | assert_eq!(response_cookie.is_some(), false); 694 | } 695 | } 696 | --------------------------------------------------------------------------------