Skip to content
Merged
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
79 changes: 0 additions & 79 deletions .cursor/rules/typescript-coding-guidelines.mdc

This file was deleted.

5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added basic unit tests
- Added publisher config
- Add AI assist rules. Based on https://github.com/hashintel/hash

### Changed
- Upgrade to rust 1.87.0
- Upgrade to fastly-cli 11.3.0
- Changed to use constants for headers
- Changed to use log statements
- Updated fastly.toml for local development
- Changed to propagate server errors as HTTP errors

### Fixed
- Rebuild when `TRUSTED_SERVER__*` env variables change

### Fixed
- Rebuild when `TRUSTED_SERVER__*` env variables change
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ cargo check
### Configuration Files
- `fastly.toml`: Fastly service configuration and build settings
- `trusted-server.toml`: Application settings (ad servers, KV stores, ID templates)
- `rust-toolchain.toml`: Pins Rust version to 1.83.0
- `rust-toolchain.toml`: Pins Rust version to 1.87.0

### Key Functionality Areas
1. **Synthetic ID Generation**: Privacy-preserving user identification using HMAC
Expand Down
55 changes: 55 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ license = "Apache-2.0"
chrono = "0.4"
config = "0.15.11"
cookie = "0.18.1"
derive_more = { version = "1.0", features = ["display", "error"] }
error-stack = "0.5"
fastly = "0.11.5"
futures = "0.3"
handlebars = "6.3.2"
Expand All @@ -30,6 +32,9 @@ url = "2.4.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.91"
config = "0.15.11"
derive_more = { version = "1.0", features = ["display", "error"] }
error-stack = "0.5"
http = "1.3.1"

[dev-dependencies]
regex = "1.1.1"
Expand Down
3 changes: 3 additions & 0 deletions crates/common/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[path = "src/error.rs"]
mod error;

#[path = "src/settings.rs"]
mod settings;

Expand Down
57 changes: 46 additions & 11 deletions crates/common/src/cookies.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
//! Cookie handling utilities.
//!
//! This module provides functionality for parsing and creating cookies
//! used in the trusted server system.

use cookie::{Cookie, CookieJar};
use error_stack::{Report, ResultExt};
use fastly::http::header;
use fastly::Request;

use crate::error::TrustedServerError;
use crate::settings::Settings;

const COOKIE_MAX_AGE: i32 = 365 * 24 * 60 * 60; // 1 year

// return empty cookie jar for unparsable cookies
/// Parses a cookie string into a [`CookieJar`].
///
/// Returns an empty jar if the cookie string is unparseable.
/// Individual invalid cookies are skipped rather than failing the entire parse.
pub fn parse_cookies_to_jar(s: &str) -> CookieJar {
let cookie_str = s.trim().to_owned();
let mut jar = CookieJar::new();
Expand All @@ -19,20 +29,39 @@ pub fn parse_cookies_to_jar(s: &str) -> CookieJar {
jar
}

pub fn handle_request_cookies(req: &Request) -> Option<CookieJar> {
/// Extracts and parses cookies from an HTTP request.
///
/// Attempts to parse the Cookie header into a [`CookieJar`] for easy access
/// to individual cookies.
///
/// # Errors
///
/// - [`TrustedServerError::InvalidHeaderValue`] if the Cookie header contains invalid UTF-8
pub fn handle_request_cookies(
req: &Request,
) -> Result<Option<CookieJar>, Report<TrustedServerError>> {
match req.get_header(header::COOKIE) {
Some(header_value) => {
let header_value_str: &str = header_value.to_str().unwrap_or("");
let jar: CookieJar = parse_cookies_to_jar(header_value_str);
Some(jar)
let header_value_str =
header_value
.to_str()
.change_context(TrustedServerError::InvalidHeaderValue {
message: "Cookie header contains invalid UTF-8".to_string(),
})?;
let jar = parse_cookies_to_jar(header_value_str);
Ok(Some(jar))
}
None => {
log::warn!("No cookie header found in request");
None
log::debug!("No cookie header found in request");
Ok(None)
}
}
}

/// Creates a synthetic ID cookie string.
///
/// Generates a properly formatted cookie with security attributes
/// for storing the synthetic ID.
pub fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> String {
format!(
"synthetic_id={}; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age={}",
Expand Down Expand Up @@ -84,7 +113,9 @@ mod tests {
#[test]
fn test_handle_request_cookies() {
let req = Request::get("http://example.com").with_header(header::COOKIE, "c1=v1;c2=v2");
let jar = handle_request_cookies(&req).unwrap();
let jar = handle_request_cookies(&req)
.expect("should parse cookies")
.expect("should have cookie jar");

assert!(jar.iter().count() == 2);
assert_eq!(jar.get("c1").unwrap().value(), "v1");
Expand All @@ -94,23 +125,27 @@ mod tests {
#[test]
fn test_handle_request_cookies_with_empty_cookie() {
let req = Request::get("http://example.com").with_header(header::COOKIE, "");
let jar = handle_request_cookies(&req).unwrap();
let jar = handle_request_cookies(&req)
.expect("should parse cookies")
.expect("should have cookie jar");

assert!(jar.iter().count() == 0);
}

#[test]
fn test_handle_request_cookies_no_cookie_header() {
let req: Request = Request::get("https://example.com");
let jar = handle_request_cookies(&req);
let jar = handle_request_cookies(&req).expect("should handle missing cookie header");

assert!(jar.is_none());
}

#[test]
fn test_handle_request_cookies_invalid_cookie_header() {
let req = Request::get("http://example.com").with_header(header::COOKIE, "invalid");
let jar = handle_request_cookies(&req).unwrap();
let jar = handle_request_cookies(&req)
.expect("should parse cookies")
.expect("should have cookie jar");

assert!(jar.iter().count() == 0);
}
Expand Down
Loading