Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make subscription and filter cookies split into multiple cookies if they're too large #288

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use std::{
str::{from_utf8, Split},
string::ToString,
};
use time::Duration;
use time::OffsetDateTime;

use crate::dbg_msg;

Expand Down Expand Up @@ -170,10 +170,8 @@ impl ResponseExt for Response<Body> {
}

fn remove_cookie(&mut self, name: String) {
let mut cookie = Cookie::from(name);
cookie.set_path("/");
cookie.set_max_age(Duration::seconds(1));
if let Ok(val) = header::HeaderValue::from_str(&cookie.to_string()) {
let removal_cookie = Cookie::build(name).path("/").http_only(true).expires(OffsetDateTime::now_utc());
if let Ok(val) = header::HeaderValue::from_str(&removal_cookie.to_string()) {
self.headers_mut().append("Set-Cookie", val);
}
}
Expand Down
116 changes: 115 additions & 1 deletion src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::collections::HashMap;

// CRATES
use crate::server::ResponseExt;
use crate::subreddit::join_until_size_limit;
use crate::utils::{redirect, template, Preferences};
use cookie::Cookie;
use futures_lite::StreamExt;
Expand Down Expand Up @@ -119,7 +120,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body

let mut response = redirect(&path);

for name in [PREFS.to_vec(), vec!["subscriptions", "filters"]].concat() {
for name in PREFS {
match form.get(name) {
Some(value) => response.insert_cookie(
Cookie::build((name.to_owned(), value.clone()))
Expand All @@ -136,6 +137,119 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
};
}

// Get subscriptions/filters to restore from query string
let subscriptions = form.get("subscriptions");
let filters = form.get("filters");

// We can't search through the cookies directly like in subreddit.rs, so instead we have to make a string out of the request's headers to search through
let cookies_string = parts
.headers
.get("cookie")
.map(|hv| hv.to_str().unwrap_or("").to_string()) // Return String
.unwrap_or_else(String::new); // Return an empty string if None

// If there are subscriptions to restore set them and delete any old subscriptions cookies, otherwise delete them all
if subscriptions.is_some() {
let sub_list: Vec<String> = subscriptions.expect("Subscriptions").split('+').map(str::to_string).collect();

// Start at 0 to keep track of what number we need to start deleting old subscription cookies from
let mut subscriptions_number_to_delete_from = 0;

// Starting at 0 so we handle the subscription cookie without a number first
for (subscriptions_number, list) in join_until_size_limit(&sub_list).into_iter().enumerate() {
let subscriptions_cookie = if subscriptions_number == 0 {
"subscriptions".to_string()
} else {
format!("subscriptions{}", subscriptions_number)
};

response.insert_cookie(
Cookie::build((subscriptions_cookie, list))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);

subscriptions_number_to_delete_from += 1;
}

// While subscriptionsNUMBER= is in the string of cookies add a response removing that cookie
while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) {
// Remove that subscriptions cookie
response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}"));

// Increment subscriptions cookie number
subscriptions_number_to_delete_from += 1;
}
} else {
// Remove unnumbered subscriptions cookie
response.remove_cookie("subscriptions".to_string());

// Starts at one to deal with the first numbered subscription cookie and onwards
let mut subscriptions_number_to_delete_from = 1;

// While subscriptionsNUMBER= is in the string of cookies add a response removing that cookie
while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) {
// Remove that subscriptions cookie
response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}"));

// Increment subscriptions cookie number
subscriptions_number_to_delete_from += 1;
}
}

// If there are filters to restore set them and delete any old filters cookies, otherwise delete them all
if filters.is_some() {
let filters_list: Vec<String> = filters.expect("Filters").split('+').map(str::to_string).collect();

// Start at 0 to keep track of what number we need to start deleting old subscription cookies from
let mut filters_number_to_delete_from = 0;

// Starting at 0 so we handle the subscription cookie without a number first
for (filters_number, list) in join_until_size_limit(&filters_list).into_iter().enumerate() {
let filters_cookie = if filters_number == 0 {
"filters".to_string()
} else {
format!("filters{}", filters_number)
};

response.insert_cookie(
Cookie::build((filters_cookie, list))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);

filters_number_to_delete_from += 1;
}

// While filtersNUMBER= is in the string of cookies add a response removing that cookie
while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) {
// Remove that filters cookie
response.remove_cookie(format!("filters{filters_number_to_delete_from}"));

// Increment filters cookie number
filters_number_to_delete_from += 1;
}
} else {
// Remove unnumbered filters cookie
response.remove_cookie("filters".to_string());

// Starts at one to deal with the first numbered subscription cookie and onwards
let mut filters_number_to_delete_from = 1;

// While filtersNUMBER= is in the string of cookies add a response removing that cookie
while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) {
// Remove that sfilters cookie
response.remove_cookie(format!("filters{filters_number_to_delete_from}"));

// Increment filters cookie number
filters_number_to_delete_from += 1;
}
}

response
}

Expand Down
138 changes: 123 additions & 15 deletions src/subreddit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,41 @@ pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default()
}

// Join items in chunks of 4000 bytes in length for cookies
pub fn join_until_size_limit<T: std::fmt::Display>(vec: &[T]) -> Vec<std::string::String> {
let mut result = Vec::new();
let mut list = String::new();
let mut current_size = 0;

for item in vec {
// Size in bytes
let item_size = item.to_string().len();
// Use 4000 bytes to leave us some headroom because the name and options of the cookie count towards the 4096 byte cap
if current_size + item_size > 4000 {
// If last item add a seperator on the end of the list so it's interpreted properly in tanden with the next cookie
list.push('+');

// Push current list to result vector
result.push(list);

// Reset the list variable so we can continue with only new items
list = String::new();
}
// Add separator if not the first item
if !list.is_empty() {
list.push('+');
}
// Add current item to list
list.push_str(&item.to_string());
current_size = list.len() + item_size;
}
// Make sure to push whatever the remaining subreddits are there into the result vector
result.push(list);

// Return resulting vector
result
}

// Sub, filter, unfilter, or unsub by setting subscription cookie using response "Set-Cookie" header
pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_default();
Expand Down Expand Up @@ -306,28 +341,101 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,

let mut response = redirect(&path);

// Delete cookie if empty, else set
// If sub_list is empty remove all subscriptions cookies, otherwise update them and remove old ones
if sub_list.is_empty() {
// Remove subscriptions cookie
response.remove_cookie("subscriptions".to_string());

// Start with first numbered subscriptions cookie
let mut subscriptions_number = 1;

// While whatever subscriptionsNUMBER cookie we're looking at has a value
while req.cookie(&format!("subscriptions{}", subscriptions_number)).is_some() {
// Remove that subscriptions cookie
response.remove_cookie(format!("subscriptions{}", subscriptions_number));

// Increment subscriptions cookie number
subscriptions_number += 1;
}
} else {
response.insert_cookie(
Cookie::build(("subscriptions", sub_list.join("+")))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);
// Start at 0 to keep track of what number we need to start deleting old subscription cookies from
let mut subscriptions_number_to_delete_from = 0;

// Starting at 0 so we handle the subscription cookie without a number first
for (subscriptions_number, list) in join_until_size_limit(&sub_list).into_iter().enumerate() {
let subscriptions_cookie = if subscriptions_number == 0 {
"subscriptions".to_string()
} else {
format!("subscriptions{}", subscriptions_number)
};

response.insert_cookie(
Cookie::build((subscriptions_cookie, list))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);

subscriptions_number_to_delete_from += 1;
}

// While whatever subscriptionsNUMBER cookie we're looking at has a value
while req.cookie(&format!("subscriptions{}", subscriptions_number_to_delete_from)).is_some() {
// Remove that subscriptions cookie
response.remove_cookie(format!("subscriptions{}", subscriptions_number_to_delete_from));

// Increment subscriptions cookie number
subscriptions_number_to_delete_from += 1;
}
}

// If filters is empty remove all filters cookies, otherwise update them and remove old ones
if filters.is_empty() {
// Remove filters cookie
response.remove_cookie("filters".to_string());

// Start with first numbered filters cookie
let mut filters_number = 1;

// While whatever filtersNUMBER cookie we're looking at has a value
while req.cookie(&format!("filters{}", filters_number)).is_some() {
// Remove that filters cookie
response.remove_cookie(format!("filters{}", filters_number));

// Increment filters cookie number
filters_number += 1;
}
} else {
response.insert_cookie(
Cookie::build(("filters", filters.join("+")))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);
// Start at 0 to keep track of what number we need to start deleting old filters cookies from
let mut filters_number_to_delete_from = 0;

for (filters_number, list) in join_until_size_limit(&filters).into_iter().enumerate() {
let filters_cookie = if filters_number == 0 {
"filters".to_string()
} else {
format!("filters{}", filters_number)
};

response.insert_cookie(
Cookie::build((filters_cookie, list))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);

filters_number_to_delete_from += 1;
}

// While whatever filtersNUMBER cookie we're looking at has a value
while req.cookie(&format!("filters{}", filters_number_to_delete_from)).is_some() {
// Remove that filters cookie
response.remove_cookie(format!("filters{}", filters_number_to_delete_from));

// Increment filters cookie number
filters_number_to_delete_from += 1;
}
}

Ok(response)
Expand Down
Loading
Loading