001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.http.security.data; 017 018import de.cuioss.http.security.core.ValidationType; 019import org.jspecify.annotations.Nullable; 020 021import java.util.Arrays; 022import java.util.List; 023import java.util.Optional; 024 025/** 026 * Immutable record representing an HTTP cookie with name, value, and attributes. 027 * 028 * <p>This record encapsulates the structure of HTTP cookies as defined in RFC 6265, 029 * providing a type-safe way to handle cookie data in HTTP security validation.</p> 030 * 031 * <h3>Design Principles</h3> 032 * <ul> 033 * <li><strong>Immutability</strong> - All fields are final and the record cannot be modified</li> 034 * <li><strong>RFC Compliance</strong> - Follows HTTP cookie specifications</li> 035 * <li><strong>Security Focus</strong> - Designed with security validation in mind</li> 036 * <li><strong>Flexibility</strong> - Supports various cookie attribute formats</li> 037 * </ul> 038 * 039 * <h3>Usage Examples</h3> 040 * <pre> 041 * // Simple cookie 042 * Cookie sessionCookie = new Cookie("JSESSIONID", "ABC123", ""); 043 * 044 * // Cookie with attributes 045 * Cookie secureCookie = new Cookie( 046 * "auth_token", 047 * "xyz789", 048 * "Domain=example.com; Path=/; Secure; HttpOnly" 049 * ); 050 * 051 * // Access components 052 * String name = cookie.name(); // "JSESSIONID" 053 * String value = cookie.value(); // "ABC123" 054 * String attrs = cookie.attributes(); // "Domain=..." 055 * 056 * // Check for security attributes 057 * boolean isSecure = cookie.isSecure(); // Check for Secure attribute 058 * boolean isHttpOnly = cookie.isHttpOnly(); // Check for HttpOnly attribute 059 * 060 * // Use in validation 061 * validator.validate(cookie.name(), ValidationType.COOKIE_NAME); 062 * validator.validate(cookie.value(), ValidationType.COOKIE_VALUE); 063 * </pre> 064 * 065 * <h3>Cookie Attributes</h3> 066 * <p>The attributes field contains the semicolon-separated list of cookie attributes 067 * such as Domain, Path, Secure, HttpOnly, SameSite, and Max-Age. This field can be 068 * an empty string if no attributes are present.</p> 069 * 070 * <h3>Security Considerations</h3> 071 * <p>This record is a simple data container. Security validation should be applied 072 * to the name, value, and attributes components separately using appropriate validators.</p> 073 * 074 * Implements: Task B3 from HTTP verification specification 075 * 076 * @param name The cookie name (e.g., "JSESSIONID", "auth_token") 077 * @param value The cookie value (e.g., session ID, authentication token) 078 * @param attributes Cookie attributes string (e.g., "Domain=example.com; Secure; HttpOnly") 079 * 080 * @since 1.0 081 * @see ValidationType#COOKIE_NAME 082 * @see ValidationType#COOKIE_VALUE 083 */ 084public record Cookie(@Nullable String name, @Nullable String value, @Nullable String attributes) { 085 086 /** 087 * Creates a simple cookie with no attributes. 088 * 089 * @param name The cookie name 090 * @param value The cookie value 091 * @return A Cookie with no attributes 092 */ 093 public static Cookie simple(String name, String value) { 094 return new Cookie(name, value, ""); 095 } 096 097 /** 098 * Checks if this cookie has a non-null, non-empty name. 099 * 100 * @return true if the name is not null and not empty 101 */ 102 public boolean hasName() { 103 return name != null && !name.isEmpty(); 104 } 105 106 /** 107 * Checks if this cookie has a non-null, non-empty value. 108 * 109 * @return true if the value is not null and not empty 110 */ 111 public boolean hasValue() { 112 return value != null && !value.isEmpty(); 113 } 114 115 /** 116 * Checks if this cookie has any attributes. 117 * 118 * @return true if the attributes string is not null and not empty 119 */ 120 public boolean hasAttributes() { 121 return attributes != null && !attributes.isEmpty(); 122 } 123 124 /** 125 * Checks if the cookie has the Secure attribute. 126 * 127 * @return true if the attributes contain "Secure" 128 */ 129 @SuppressWarnings("ConstantConditions") 130 public boolean isSecure() { 131 return hasAttributes() && attributes.toLowerCase().contains("secure"); 132 } 133 134 /** 135 * Checks if the cookie has the HttpOnly attribute. 136 * 137 * @return true if the attributes contain "HttpOnly" 138 */ 139 @SuppressWarnings("ConstantConditions") 140 public boolean isHttpOnly() { 141 return hasAttributes() && attributes.toLowerCase().contains("httponly"); 142 } 143 144 /** 145 * Extracts the Domain attribute value if present. 146 * 147 * @return The domain value wrapped in Optional, or empty if not specified 148 */ 149 public Optional<String> getDomain() { 150 return extractAttributeValue("domain"); 151 } 152 153 /** 154 * Extracts the Path attribute value if present. 155 * 156 * @return The path value wrapped in Optional, or empty if not specified 157 */ 158 public Optional<String> getPath() { 159 return extractAttributeValue("path"); 160 } 161 162 /** 163 * Extracts the SameSite attribute value if present. 164 * 165 * @return The SameSite value (e.g., "Strict", "Lax", "None") wrapped in Optional, or empty if not specified 166 */ 167 public Optional<String> getSameSite() { 168 return extractAttributeValue("samesite"); 169 } 170 171 /** 172 * Extracts the Max-Age attribute value if present. 173 * 174 * @return The Max-Age value as a string wrapped in Optional, or empty if not specified 175 */ 176 public Optional<String> getMaxAge() { 177 return extractAttributeValue("max-age"); 178 } 179 180 /** 181 * Extracts a specific attribute value from the attributes string. 182 * 183 * @param attributeName The name of the attribute (case-insensitive) 184 * @return The attribute value or null if not found 185 */ 186 private Optional<String> extractAttributeValue(String attributeName) { 187 if (!hasAttributes()) { 188 return Optional.empty(); 189 } 190 return AttributeParser.extractAttributeValue(attributes, attributeName); 191 } 192 193 /** 194 * Returns all attribute names present in this cookie. 195 * 196 * @return A list of attribute names (may be empty) 197 */ 198 @SuppressWarnings("ConstantConditions") 199 public List<String> getAttributeNames() { 200 if (!hasAttributes()) { 201 return List.of(); 202 } 203 return Arrays.stream(attributes.split(";")) 204 .map(String::trim) 205 .filter(attr -> !attr.isEmpty()) 206 .map(attr -> { 207 int equalIndex = attr.indexOf('='); 208 return equalIndex > 0 ? attr.substring(0, equalIndex).trim() : attr; 209 }) 210 .toList(); 211 } 212 213 public String nameOrDefault(String defaultName) { 214 return name != null ? name : defaultName; 215 } 216 217 public String valueOrDefault(String defaultValue) { 218 return value != null ? value : defaultValue; 219 } 220 221 /** 222 * Returns a string representation suitable for HTTP Set-Cookie headers. 223 * Note: This does not perform proper HTTP encoding - use appropriate 224 * encoding utilities for actual HTTP header generation. 225 * 226 * @return A string in the format "name=value; attributes" 227 */ 228 public String toCookieString() { 229 StringBuilder sb = new StringBuilder(); 230 231 if (name != null) { 232 sb.append(name); 233 } 234 235 sb.append("="); 236 237 if (value != null) { 238 sb.append(value); 239 } 240 241 if (hasAttributes()) { 242 sb.append("; ").append(attributes); 243 } 244 245 return sb.toString(); 246 } 247 248 /** 249 * Returns a copy of this cookie with a new name. 250 * 251 * @param newName The new cookie name 252 * @return A new Cookie with the specified name and same value/attributes 253 */ 254 public Cookie withName(String newName) { 255 return new Cookie(newName, value, attributes); 256 } 257 258 /** 259 * Returns a copy of this cookie with a new value. 260 * 261 * @param newValue The new cookie value 262 * @return A new Cookie with the same name/attributes and specified value 263 */ 264 public Cookie withValue(String newValue) { 265 return new Cookie(name, newValue, attributes); 266 } 267 268 /** 269 * Returns a copy of this cookie with new attributes. 270 * 271 * @param newAttributes The new attributes string 272 * @return A new Cookie with the same name/value and specified attributes 273 */ 274 public Cookie withAttributes(String newAttributes) { 275 return new Cookie(name, value, newAttributes); 276 } 277}