From 5d61487d7bb216e18eb12d63b73281f6df779019 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Fri, 10 Oct 2025 18:59:06 +0800 Subject: [PATCH 01/15] [fit] Add CookieAttributeNames class with RFC 6265/6265bis Set-Cookie attribute constants --- .../http/protocol/CookieAttributeNames.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/CookieAttributeNames.java diff --git a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/CookieAttributeNames.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/CookieAttributeNames.java new file mode 100644 index 00000000..634bdca4 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/CookieAttributeNames.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.protocol; + +/** + * RFC 6265 等规范中定义的 Set-Cookie 属性名称常量。 + *

RFC 6265 列出了很多属性名称的定义来源。

+ * + * @author 徐吴昊 + * @since 2025-09-24 + */ +public class CookieAttributeNames { + /** @see RFC 6265 */ + public static final String EXPIRES = "Expires"; + + /** @see RFC 6265 */ + public static final String MAX_AGE = "Max-Age"; + + /** @see RFC 6265 */ + public static final String DOMAIN = "Domain"; + + /** @see RFC 6265 */ + public static final String PATH = "Path"; + + /** @see RFC 6265 */ + public static final String SECURE = "Secure"; + + /** @see RFC 6265 */ + public static final String HTTP_ONLY = "HttpOnly"; + + /** @see RFC 6265bis */ + public static final String SAME_SITE = "SameSite"; +} From ee1a575d227a16f422ca5c660ed8e572ec4cb0f2 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Fri, 10 Oct 2025 18:59:18 +0800 Subject: [PATCH 02/15] [fit] add SameSite attribute to Cookie and deprecate version/comment methods --- .../java/modelengine/fit/http/Cookie.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java index 8a3736a1..ffc54c09 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java @@ -35,6 +35,7 @@ public interface Cookie { * * @return 表示 Cookie 版本号的 {@code int}。 */ + @Deprecated int version(); /** @@ -43,6 +44,7 @@ public interface Cookie { * * @return 表示 Cookie 注释的 {@link String}。 */ + @Deprecated String comment(); /** @@ -79,12 +81,20 @@ public interface Cookie { /** * 判断 Cookie 是否仅允许在服务端获取。 - *

该属性并不是 Cookie 的标准,但是被浏览器支持。

+ *

其 HttpOnly 属性的格式为 {@code ;HttpOnly ...},如果存在则表示仅服务端可访问。

* - * @return 如果 Cookie 仅允许在服务端获取,返回 {@code true},否则,返回 {@code false}。 + * @return 如果 Cookie 仅允许在服务端访问,则返回 {@code true},否则返回 {@code false}。 */ boolean httpOnly(); + /** + * 获取 Cookie 的 SameSite 属性。 + *

其 SameSite 属性的格式为 {@code ;SameSite=VALUE ...},表示跨站请求策略。

+ * + * @return SameSite 值,如 {@code "Strict"}、{@code "Lax"}、{@code "None"}。 + */ + String sameSite(); + /** * {@link Cookie} 的构建器。 */ @@ -111,6 +121,7 @@ interface Builder { * @param version 表示待设置的 Cookie 版本的 {@code int}。 * @return 表示当前构建器的 {@link Builder}。 */ + @Deprecated Builder version(int version); /** @@ -119,6 +130,7 @@ interface Builder { * @param comment 表示待设置的 Cookie 注释的 {@link String}。 * @return 表示当前构建器的 {@link Builder}。 */ + @Deprecated Builder comment(String comment); /** @@ -161,6 +173,14 @@ interface Builder { */ Builder httpOnly(boolean httpOnly); + /** + * 向当前构建器中设置 Cookie 限制跨站请求时发送行为安全级别。 + * + * @param sameSite SameSite 值,如 "Strict", "Lax", "None"。 + * @return 表示当前构建器的 {@link Builder}。 + */ + Builder sameSite(String sameSite); + /** * 构建对象。 * From be0d2a84d0bd20f1a4a93eb2e5fbcc2477233277 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Fri, 10 Oct 2025 19:00:11 +0800 Subject: [PATCH 03/15] [fit] add Cookie utility methods and corresponding unit tests --- .../modelengine/fit/http/util/HttpUtils.java | 163 ++++++++++++++++++ .../fit/http/util/HttpUtilsTest.java | 94 ++++++++++ 2 files changed, 257 insertions(+) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index 43fd215d..2a580118 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -6,8 +6,15 @@ package modelengine.fit.http.util; +import static modelengine.fit.http.protocol.CookieAttributeNames.DOMAIN; +import static modelengine.fit.http.protocol.CookieAttributeNames.HTTP_ONLY; +import static modelengine.fit.http.protocol.CookieAttributeNames.MAX_AGE; +import static modelengine.fit.http.protocol.CookieAttributeNames.PATH; +import static modelengine.fit.http.protocol.CookieAttributeNames.SAME_SITE; +import static modelengine.fit.http.protocol.CookieAttributeNames.SECURE; import static modelengine.fitframework.inspection.Validation.notNull; +import modelengine.fit.http.Cookie; import modelengine.fit.http.header.HeaderValue; import modelengine.fit.http.header.ParameterCollection; import modelengine.fit.http.header.support.DefaultHeaderValue; @@ -21,9 +28,16 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.time.Duration; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; /** * Http 协议相关的工具类。 @@ -36,6 +50,155 @@ public class HttpUtils { private static final char STRING_VALUE_SURROUNDED = '\"'; + /** + * 将给定的 {@link Cookie} 对象格式化为符合 HTTP 协议的 {@code Set-Cookie} 头部字符串。 + *

生成结果遵循 RFC 6265 规范,如果 cookie 对象为空,则返回空字符串

+ * + * @param cookie 表示待格式化的 {@link Cookie} 对象。 + * @return 表示生成的 {@code Set-Cookie} 头部字符串的 {@link String}。 + */ + public static String formatSetCookie(Cookie cookie) { + if (cookie == null || StringUtils.isBlank(cookie.name())) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append(cookie.name()).append("=").append(cookie.value() != null ? cookie.value() : ""); + if (cookie.path() != null && !cookie.path().isEmpty()) { + sb.append("; ").append(PATH).append("=").append(cookie.path()); + } + if (cookie.domain() != null && !cookie.domain().isEmpty()) { + sb.append("; ").append(DOMAIN).append("=").append(cookie.domain()); + } + if (cookie.maxAge() >= 0) { + sb.append("; ").append(MAX_AGE).append("=").append(cookie.maxAge()); + } + if (cookie.secure()) { + sb.append("; ").append(SECURE); + } + if (cookie.httpOnly()) { + sb.append("; ").append(HTTP_ONLY); + } + if (cookie.sameSite() != null && !cookie.sameSite().isEmpty()) { + sb.append("; ").append(SAME_SITE).append("=").append(cookie.sameSite()); + } + return sb.toString(); + } + + /** + * 从消息头 Set-Cookie 的字符串值中解析 Cookie 的值以及属性。 + *

若包含 Expires 属性,则会自动换算为 Max-Age。

+ * + * @param rawCookie 表示待解析的 Set-Cookie 字符串值的 {@link String}。 + * @return 表示解析后的 {@link Cookie}。 + */ + public static Cookie parseSetCookie(String rawCookie) { + if (StringUtils.isBlank(rawCookie)) { + return Cookie.builder().build(); + } + + Cookie.Builder builder = Cookie.builder(); + + String[] parts = rawCookie.split(";"); + + String[] nameValue = parts[0].split("=", 2); + builder.name(nameValue[0].trim()); + builder.value(nameValue.length > 1 ? nameValue[1].trim() : ""); + + for (int i = 1; i < parts.length; i++) { + String part = parts[i].trim(); + if (part.isEmpty()) { + continue; + } + String[] kv = part.split("=", 2); + String key = kv[0].trim().toLowerCase(Locale.ROOT); + String val = kv.length > 1 ? kv[1].trim() : ""; + + switch (key) { + case "path": + builder.path(val); + break; + case "domain": + builder.domain(val); + break; + case "max-age": + try { + builder.maxAge(Integer.parseInt(val)); + } catch (NumberFormatException ignore) { + } + break; + case "expires": + int maxAge = convertExpiresToMaxAge(val); + builder.maxAge(maxAge); + break; + case "secure": + builder.secure(true); + break; + case "httponly": + builder.httpOnly(true); + break; + case "samesite": + builder.sameSite(val); + break; + default: + break; + } + } + return builder.build(); + } + + /** + * 从 Cookie 头部字符串解析多个 Cookie。 + *

示例:{@code "a=1; b=2; c=3"} → List[Cookie(a=1), Cookie(b=2), Cookie(c=3)]

+ * + * @param rawCookie 表示原始 Cookie 头的字符串的 {@link String}(例如 "a=1; b=2; c=3")。 + * @return 表示解析得到的 Cookie 列表的 {@link List}{@code <}{@link Cookie}{@code >}。 + */ + public static List parseCookies(String rawCookie) { + if (rawCookie == null || rawCookie.isEmpty()) { + return Collections.emptyList(); + } + String[] pairs = rawCookie.split(";"); + List cookies = new ArrayList<>(); + + for (String pair : pairs) { + String trimmed = pair.trim(); + if (trimmed.isEmpty()) { + continue; + } + + int eqIndex = trimmed.indexOf('='); + if (eqIndex <= 0) { + continue; + } + + String name = trimmed.substring(0, eqIndex).trim(); + String value = trimmed.substring(eqIndex + 1).trim(); + + if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) { + value = value.substring(1, value.length() - 1); + } + + cookies.add(Cookie.builder().name(name).value(value).build()); + } + + return cookies; + } + + private static int convertExpiresToMaxAge(String expiresString) { + if (StringUtils.isBlank(expiresString)) { + return -1; + } + + try { + ZonedDateTime expires = + ZonedDateTime.parse(expiresString, DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.US)); + long seconds = Duration.between(ZonedDateTime.now(ZoneOffset.UTC), expires).getSeconds(); + return (int) Math.max(seconds, 0); + } catch (DateTimeParseException e) { + return -1; + } + } + /** * 从消息头的字符串值中解析消息头的值。 * diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java index f0ae44a9..6fa78cab 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowableOfType; +import modelengine.fit.http.Cookie; import modelengine.fit.http.header.HeaderValue; import modelengine.fitframework.util.StringUtils; @@ -17,6 +18,10 @@ import java.net.MalformedURLException; import java.net.URL; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; /** * 为 {@link HttpUtils} 提供单元测试。 @@ -26,6 +31,95 @@ */ @DisplayName("测试 HttpUtils 工具类") public class HttpUtilsTest { + @Test + @DisplayName("格式化完整 Set-Cookie,返回正确格式化字符串") + void givenFullCookie_thenIncludeAllAttributes() { + Cookie cookie = Cookie.builder() + .name("token") + .value("abc") + .path("/api") + .domain("example.com") + .maxAge(3600) + .secure(true) + .httpOnly(true) + .sameSite("Lax") + .build(); + + String result = HttpUtils.formatSetCookie(cookie); + assertThat(result).contains("token=abc") + .contains("Path=/api") + .contains("Domain=example.com") + .contains("Max-Age=3600") + .contains("Secure") + .contains("HttpOnly") + .contains("SameSite=Lax"); + } + + @Test + @DisplayName("格式化空的 Set-Cookie,返回空字符串") + void givenNullCookie_thenReturnEmptyString() { + assertThat(HttpUtils.formatSetCookie(null)).isEmpty(); + } + + @Test + @DisplayName("解析合法的 Set-Cookie 值,返回正确的 Cookie 对象") + void givenValidSetCookieStringThenParseSuccessfully() { + String rawCookie = "ID=ab12xy; Path=/; Domain=example.com; Max-Age=3600; Secure; SameSite=Strict"; + Cookie cookie = HttpUtils.parseSetCookie(rawCookie); + + assertThat(cookie.name()).isEqualTo("ID"); + assertThat(cookie.value()).isEqualTo("ab12xy"); + assertThat(cookie.path()).isEqualTo("/"); + assertThat(cookie.domain()).isEqualTo("example.com"); + assertThat(cookie.maxAge()).isEqualTo(3600); + assertThat(cookie.secure()).isTrue(); + assertThat(cookie.httpOnly()).isFalse(); + assertThat(cookie.sameSite()).isEqualTo("Strict"); + } + + @Test + @DisplayName("给定空的 Set-Cookie 值,返回空的 Cookie 对象") + void givenEmptySetCookieThenReturnEmptyCookie() { + Cookie cookie = HttpUtils.parseSetCookie(""); + assertThat(cookie.name()).isNull(); + assertThat(cookie.value()).isNull(); + } + + @Test + @DisplayName("解析带 Expires 属性的 Set-Cookie,自动换算为 Max-Age") + void givenExpiresAttributeThenConvertToMaxAge() { + ZonedDateTime expiresTime = ZonedDateTime.now(ZoneOffset.UTC).plusHours(1); + String expiresStr = expiresTime.format(DateTimeFormatter.RFC_1123_DATE_TIME); + String rawCookie = "ID=xyz; Expires=" + expiresStr; + + Cookie cookie = HttpUtils.parseSetCookie(rawCookie); + assertThat(cookie.maxAge()).isBetween(3500, 3700); + } + + @Test + @DisplayName("解析多个 Cookie 头部值,返回正确的 Cookie 列表") + void givenMultipleCookiesThenReturnCookieList() { + String rawCookie = "a=1; b=2; c=3"; + List cookies = HttpUtils.parseCookies(rawCookie); + + assertThat(cookies).hasSize(3); + assertThat(cookies.get(0).name()).isEqualTo("a"); + assertThat(cookies.get(0).value()).isEqualTo("1"); + assertThat(cookies.get(1).name()).isEqualTo("b"); + assertThat(cookies.get(2).value()).isEqualTo("3"); + } + + @Test + @DisplayName("解析非法格式的 Cookie 值,自动跳过无效项") + void givenInvalidCookieStringThenSkipInvalidPairs() { + String rawCookie = "a=1; invalid; b=2"; + List cookies = HttpUtils.parseCookies(rawCookie); + + assertThat(cookies).hasSize(2); + assertThat(cookies.get(0).name()).isEqualTo("a"); + assertThat(cookies.get(1).name()).isEqualTo("b"); + } + @Test @DisplayName("给定空的值,解析消息头的值返回为空") void givenEmptyValueThenReturnHeaderValueISEmpty() { From a53883a2393c1bbed90f39b1330b616d92a30569 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Fri, 10 Oct 2025 19:00:46 +0800 Subject: [PATCH 04/15] [fit] update CookieCollection design and improve unit tests --- .../fit/http/header/CookieCollection.java | 27 +++++++- .../http/support/DefaultCookieCollection.java | 57 +++++++++++++---- .../ConfigurableCookieCollectionTest.java | 64 +++++++++++++++++++ 3 files changed, 136 insertions(+), 12 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java index 5f6eae61..1df98be1 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java @@ -20,6 +20,7 @@ public interface CookieCollection extends HeaderValue { /** * 获取指定名字的 {@link Cookie}。 + *

如果存在多个同名 Cookie,返回第一个匹配的 Cookie。

* * @param name 表示 Cookie 名字的 {@link Optional}{@code <}{@link String}{@code >}。 * @return 表示指定名字的 {@link Cookie}。 @@ -27,7 +28,15 @@ public interface CookieCollection extends HeaderValue { Optional get(String name); /** - * 获取所有的 {@link Cookie}。 + * 根据名字查找所有匹配的 {@link Cookie}。 + * + * @param name 表示 Cookie 名字的 {@link String}。 + * @return 返回所有匹配名字的 {@link Cookie} 列表。 + */ + List findByName(String name); + + /** + * 获取集合中所有的 {@link Cookie}。 * * @return 表示所有 {@link Cookie} 列表的 {@link List}{@code <}{@link Cookie}{@code >}。 */ @@ -39,4 +48,20 @@ public interface CookieCollection extends HeaderValue { * @return 表示所有 {@link Cookie} 的数量的 {@code int}。 */ int size(); + + /** + * 将集合转换为 HTTP 请求头中 Cookie 形式的字符串。 + *

格式为 {@code name1=value1; name2=value2; ...}。

+ * + * @return 表示请求头的字符串。 + */ + String toRequestHeader(); + + /** + * 将集合转换为 HTTP 响应头形式的字符串列表。 + *

每个 Cookie 对应一个 {@code Set-Cookie: ...} 头。

+ * + * @return 表示响应头列表的 {@link List}{@code <}{@link String}{@code >}。 + */ + List toResponseHeaders(); } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index 1df0f9fd..f7ec2463 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -14,10 +14,15 @@ import modelengine.fit.http.header.HeaderValue; import modelengine.fit.http.header.support.DefaultHeaderValue; import modelengine.fit.http.header.support.DefaultParameterCollection; +import modelengine.fit.http.util.HttpUtils; import modelengine.fitframework.util.StringUtils; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -28,6 +33,8 @@ * @since 2022-07-06 */ public class DefaultCookieCollection extends DefaultHeaderValue implements ConfigurableCookieCollection { + private final Map> store = new LinkedHashMap<>(); + /** * 初始化 {@link DefaultCookieCollection} 的新实例。 */ @@ -43,33 +50,61 @@ public DefaultCookieCollection() { */ public DefaultCookieCollection(HeaderValue headerValue) { super(notNull(headerValue, "The header value cannot be null.").value(), headerValue.parameters()); + HttpUtils.parseCookies(headerValue.value()).forEach(this::add); } @Override public Optional get(String name) { - return this.parameters().get(name).map(value -> Cookie.builder().name(name).value(value).build()); + List cookies = store.get(name); + if (cookies == null || cookies.isEmpty()) { + return Optional.empty(); + } + return Optional.of(cookies.get(0)); + } + + @Override + public List findByName(String name) { + return store.getOrDefault(name, Collections.emptyList()); } @Override public List all() { - return Collections.unmodifiableList(this.parameters() - .keys() + return store.values() .stream() - .map(this::get) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList())); + .flatMap(List::stream) + .collect(Collectors.toList()); } @Override public int size() { - return this.parameters().size(); + return store.values() + .stream() + .mapToInt(List::size) + .sum(); } @Override public void add(Cookie cookie) { - if (cookie != null) { - this.parameters().set(cookie.name(), cookie.value()); - } + store.computeIfAbsent(cookie.name(), k -> new ArrayList<>()); + List list = store.get(cookie.name()); + list.removeIf(c -> + Objects.equals(c.path(), cookie.path()) && + Objects.equals(c.domain(), cookie.domain()) + ); + list.add(cookie); + } + + @Override + public String toRequestHeader() { + return all().stream() + .map(c -> c.name() + "=" + c.value()) + .collect(Collectors.joining("; ")); + } + + @Override + public List toResponseHeaders() { + return all().stream() + .map(HttpUtils::formatSetCookie) + .collect(Collectors.toList()); } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java index 945cf51e..068670c0 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java @@ -53,4 +53,68 @@ void shouldReturnAllCookie() { final List cookies = cookieCollection.all(); assertThat(cookies).hasSize(1); } + + @Test + @DisplayName("同名 Cookie 不同路径可共存") + void shouldAllowMultipleCookiesWithDifferentPath() { + ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(); + collection.add(Cookie.builder().name("user").value("A").path("/a").build()); + collection.add(Cookie.builder().name("user").value("B").path("/b").build()); + + List sameNameCookies = collection.findByName("user"); + assertThat(sameNameCookies).hasSize(2); + } + + @Test + @DisplayName("同名同 path/domain 的 Cookie 应被替换") + void shouldReplaceCookieWithSamePathAndDomain() { + ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(); + + Cookie c1 = Cookie.builder().name("id").value("1").path("/").domain("a.com").build(); + Cookie c2 = Cookie.builder().name("id").value("2").path("/").domain("a.com").build(); + + collection.add(c1); + collection.add(c2); + + List cookies = collection.findByName("id"); + assertThat(cookies).hasSize(1); + assertThat(cookies.get(0).value()).isEqualTo("2"); + } + + @Test + @DisplayName("toRequestHeader 生成单行请求头") + void shouldGenerateRequestHeader() { + ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(); + collection.add(Cookie.builder().name("a").value("1").build()); + collection.add(Cookie.builder().name("b").value("2").build()); + + String header = collection.toRequestHeader(); + assertThat(header).isEqualTo("a=1; b=2"); + } + + @Test + @DisplayName("toResponseHeaders 生成多个 Set-Cookie 响应头") + void shouldGenerateMultipleResponseHeaders() { + ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(); + collection.add(Cookie.builder().name("token").value("xyz").secure(true).httpOnly(true).build()); + collection.add(Cookie.builder().name("lang").value("zh-CN").sameSite("Lax").build()); + + List headers = collection.toResponseHeaders(); + + assertThat(headers).hasSize(2); + assertThat(headers.get(0)).contains("token=xyz"); + assertThat(headers.get(1)).contains("lang=zh-CN"); + assertThat(headers.get(1)).contains("SameSite=Lax"); + } + + @Test + @DisplayName("从 HeaderValue 初始化应正确解析多个 Cookie") + void shouldInitializeFromHeaderValue() { + String header = "a=1; b=2; c=3"; + ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(HeaderValue.create(header)); + + assertThat(collection.size()).isEqualTo(3); + assertThat(collection.get("b")).isPresent(); + assertThat(collection.get("b").get().value()).isEqualTo("2"); + } } From 1b91a6ad78c89084befb3ca5a92825e29910dfc9 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Fri, 10 Oct 2025 19:06:58 +0800 Subject: [PATCH 05/15] [fit] redesign Cookie parsing and mutability for Request/Response --- .../support/DefaultHttpClassicClientRequest.java | 8 +++++++- .../DefaultHttpClassicServerResponse.java | 10 ++++++++-- .../http/support/AbstractHttpClassicRequest.java | 14 ++++++++++++++ .../support/AbstractHttpClassicResponse.java | 16 ++++++++++++++++ .../fit/http/support/AbstractHttpMessage.java | 10 ---------- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java index db2062f1..96302f64 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java @@ -21,6 +21,7 @@ import modelengine.fit.http.entity.ReadableBinaryEntity; import modelengine.fit.http.entity.support.DefaultMultiValueEntity; import modelengine.fit.http.entity.support.DefaultObjectEntity; +import modelengine.fit.http.header.ConfigurableCookieCollection; import modelengine.fit.http.header.ContentType; import modelengine.fit.http.protocol.ClientRequest; import modelengine.fit.http.protocol.ClientResponse; @@ -155,7 +156,7 @@ protected void commit() { if (this.isCommitted()) { return; } - this.headers().set(COOKIE, this.cookies().toString()); + this.headers().set(COOKIE, this.cookies().toRequestHeader()); super.commit(); } @@ -170,4 +171,9 @@ private void close() { // Ignore } } + + @Override + public ConfigurableCookieCollection cookies() { + return (ConfigurableCookieCollection) super.cookies(); + } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java index 48687b10..f2f65ce6 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java @@ -10,7 +10,7 @@ import static modelengine.fit.http.protocol.MessageHeaderNames.CONNECTION; import static modelengine.fit.http.protocol.MessageHeaderNames.CONTENT_DISPOSITION; import static modelengine.fit.http.protocol.MessageHeaderNames.CONTENT_LENGTH; -import static modelengine.fit.http.protocol.MessageHeaderNames.COOKIE; +import static modelengine.fit.http.protocol.MessageHeaderNames.SET_COOKIE; import static modelengine.fit.http.protocol.MessageHeaderNames.TRANSFER_ENCODING; import static modelengine.fit.http.protocol.MessageHeaderValues.CHUNKED; import static modelengine.fit.http.protocol.MessageHeaderValues.KEEP_ALIVE; @@ -26,6 +26,7 @@ import modelengine.fit.http.entity.TextEventStreamEntity; import modelengine.fit.http.entity.WritableBinaryEntity; import modelengine.fit.http.entity.support.DefaultWritableBinaryEntity; +import modelengine.fit.http.header.ConfigurableCookieCollection; import modelengine.fit.http.header.ContentDisposition; import modelengine.fit.http.header.ContentType; import modelengine.fit.http.header.HeaderValue; @@ -255,7 +256,7 @@ protected void commit() { if (this.isCommitted()) { return; } - this.headers().set(COOKIE, this.cookies().toString()); + this.headers().set(SET_COOKIE, this.cookies().toResponseHeaders()); if (this.entity != null) { this.setContentTypeByEntity(this.headers(), this.entity); if (this.entity instanceof FileEntity) { @@ -280,4 +281,9 @@ private void close0() throws IOException { this.entity = null; } } + + @Override + public ConfigurableCookieCollection cookies() { + return (ConfigurableCookieCollection) super.cookies(); + } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java index a7d0c0fc..7371e989 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java @@ -6,11 +6,15 @@ package modelengine.fit.http.support; +import static modelengine.fit.http.protocol.MessageHeaderNames.COOKIE; import static modelengine.fit.http.protocol.MessageHeaderNames.HOST; import static modelengine.fitframework.inspection.Validation.notNull; import modelengine.fit.http.HttpClassicRequest; import modelengine.fit.http.HttpResource; +import modelengine.fit.http.header.ConfigurableCookieCollection; +import modelengine.fit.http.header.CookieCollection; +import modelengine.fit.http.header.HeaderValue; import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.MessageHeaderNames; import modelengine.fit.http.protocol.MessageHeaders; @@ -24,8 +28,11 @@ * @since 2022-11-23 */ public abstract class AbstractHttpClassicRequest extends AbstractHttpMessage implements HttpClassicRequest { + private static final String COOKIE_DELIMITER = ";"; + private final RequestLine startLine; private final MessageHeaders headers; + private final ConfigurableCookieCollection cookies; /** * 创建经典的 Http 请求对象。 @@ -38,6 +45,8 @@ public AbstractHttpClassicRequest(HttpResource httpResource, RequestLine startLi super(httpResource, startLine, headers); this.startLine = notNull(startLine, "The request line cannot be null."); this.headers = notNull(headers, "The message headers cannot be null."); + String actualCookie = String.join(COOKIE_DELIMITER, this.headers.all(COOKIE)); + this.cookies = ConfigurableCookieCollection.create(HeaderValue.create(actualCookie)); } @Override @@ -66,4 +75,9 @@ public String path() { public QueryCollection queries() { return this.startLine.queries(); } + + @Override + public CookieCollection cookies() { + return this.cookies; + } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java index 75c29ec9..bf1d54b3 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java @@ -6,13 +6,19 @@ package modelengine.fit.http.support; +import static modelengine.fit.http.protocol.MessageHeaderNames.SET_COOKIE; import static modelengine.fitframework.inspection.Validation.notNull; import modelengine.fit.http.HttpClassicResponse; import modelengine.fit.http.HttpResource; +import modelengine.fit.http.header.ConfigurableCookieCollection; +import modelengine.fit.http.header.CookieCollection; import modelengine.fit.http.protocol.MessageHeaders; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.StatusLine; +import modelengine.fit.http.util.HttpUtils; + +import java.util.List; /** * {@link HttpClassicResponse} 的默认实现。 @@ -22,6 +28,7 @@ */ public abstract class AbstractHttpClassicResponse extends AbstractHttpMessage implements HttpClassicResponse { private final StatusLine startLine; + private final ConfigurableCookieCollection cookies; /** * 创建经典的 Http 响应对象。 @@ -33,6 +40,10 @@ public abstract class AbstractHttpClassicResponse extends AbstractHttpMessage im public AbstractHttpClassicResponse(HttpResource httpResource, StatusLine startLine, MessageHeaders headers) { super(httpResource, startLine, headers); this.startLine = notNull(startLine, "The status line cannot be null."); + MessageHeaders headers1 = notNull(headers, "The headers cannot be null."); + List actualCookies = headers1.all(SET_COOKIE); + this.cookies = ConfigurableCookieCollection.create(); + actualCookies.stream().map(HttpUtils::parseSetCookie).forEach(cookies::add); } @Override @@ -44,4 +55,9 @@ public int statusCode() { public String reasonPhrase() { return this.startLine.reasonPhrase(); } + + @Override + public CookieCollection cookies() { + return this.cookies; + } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java index 631ddd48..668f7fad 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java @@ -47,14 +47,11 @@ * @since 2022-08-03 */ public abstract class AbstractHttpMessage implements HttpMessage { - private static final String COOKIE_DELIMITER = ";"; - private final ParameterCollection parameters = ParameterCollection.create().set(DefaultContentType.CHARSET, StandardCharsets.UTF_8.name()); private final HttpResource httpResource; private final StartLine startLine; private final MessageHeaders headers; - private final ConfigurableCookieCollection cookies; private final Map> customEntitySerializers = new HashMap<>(); private ObjectSerializer customJsonSerializer; private boolean isCommitted; @@ -70,8 +67,6 @@ protected AbstractHttpMessage(HttpResource httpResource, StartLine startLine, Me this.httpResource = notNull(httpResource, "The http resource cannot be null."); this.startLine = notNull(startLine, "The start line cannot be null."); this.headers = notNull(headers, "The message headers cannot be null."); - String actualCookie = String.join(COOKIE_DELIMITER, this.headers.all(COOKIE)); - this.cookies = ConfigurableCookieCollection.create(HttpUtils.parseHeaderValue(actualCookie)); } @Override @@ -140,11 +135,6 @@ public int contentLength() { return Integer.parseInt(value); } - @Override - public ConfigurableCookieCollection cookies() { - return this.cookies; - } - /** * 提交当前的 Http 消息。 *

提交之后的 Http 消息将无法修改。

From fc319e9e98719f3733277509f38f3a2ec401d5a2 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Fri, 10 Oct 2025 19:46:11 +0800 Subject: [PATCH 06/15] [fit] extract cookie parsing symbols as constants and optimize parsing logic --- .../modelengine/fit/http/util/HttpUtils.java | 147 +++++++++--------- 1 file changed, 77 insertions(+), 70 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index 2a580118..1d414e7e 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -7,6 +7,7 @@ package modelengine.fit.http.util; import static modelengine.fit.http.protocol.CookieAttributeNames.DOMAIN; +import static modelengine.fit.http.protocol.CookieAttributeNames.EXPIRES; import static modelengine.fit.http.protocol.CookieAttributeNames.HTTP_ONLY; import static modelengine.fit.http.protocol.CookieAttributeNames.MAX_AGE; import static modelengine.fit.http.protocol.CookieAttributeNames.PATH; @@ -49,6 +50,17 @@ public class HttpUtils { private static final char STRING_VALUE_SURROUNDED = '\"'; + private static final String COOKIES_FORMAT_SEPARATOR = "; "; + private static final String COOKIES_PARSE_SEPARATOR = ";"; + private static final String COOKIE_PAIR_SEPARATOR = "="; + + private static final String PATH_KEY = PATH.toLowerCase(Locale.ROOT); + private static final String DOMAIN_KEY = DOMAIN.toLowerCase(Locale.ROOT); + private static final String MAX_AGE_KEY = MAX_AGE.toLowerCase(Locale.ROOT); + private static final String EXPIRES_KEY = EXPIRES.toLowerCase(Locale.ROOT); + private static final String SECURE_KEY = SECURE.toLowerCase(Locale.ROOT); + private static final String HTTP_ONLY_KEY = HTTP_ONLY.toLowerCase(Locale.ROOT); + private static final String SAME_SITE_KEY = SAME_SITE.toLowerCase(Locale.ROOT); /** * 将给定的 {@link Cookie} 对象格式化为符合 HTTP 协议的 {@code Set-Cookie} 头部字符串。 @@ -59,27 +71,33 @@ public class HttpUtils { */ public static String formatSetCookie(Cookie cookie) { if (cookie == null || StringUtils.isBlank(cookie.name())) { - return ""; + return StringUtils.EMPTY; } - StringBuilder sb = new StringBuilder(); - sb.append(cookie.name()).append("=").append(cookie.value() != null ? cookie.value() : ""); - if (cookie.path() != null && !cookie.path().isEmpty()) { - sb.append("; ").append(PATH).append("=").append(cookie.path()); + + StringBuilder sb = new StringBuilder().append(cookie.name()) + .append(COOKIE_PAIR_SEPARATOR) + .append(cookie.value() != null ? cookie.value() : StringUtils.EMPTY); + + if (StringUtils.isNotBlank(cookie.path())) { + sb.append(COOKIES_FORMAT_SEPARATOR).append(PATH).append(COOKIE_PAIR_SEPARATOR).append(cookie.path()); } - if (cookie.domain() != null && !cookie.domain().isEmpty()) { - sb.append("; ").append(DOMAIN).append("=").append(cookie.domain()); + if (StringUtils.isNotBlank(cookie.domain())) { + sb.append(COOKIES_FORMAT_SEPARATOR).append(DOMAIN).append(COOKIE_PAIR_SEPARATOR).append(cookie.domain()); } if (cookie.maxAge() >= 0) { - sb.append("; ").append(MAX_AGE).append("=").append(cookie.maxAge()); + sb.append(COOKIES_FORMAT_SEPARATOR).append(MAX_AGE).append(COOKIE_PAIR_SEPARATOR).append(cookie.maxAge()); } if (cookie.secure()) { - sb.append("; ").append(SECURE); + sb.append(COOKIES_FORMAT_SEPARATOR).append(SECURE); } if (cookie.httpOnly()) { - sb.append("; ").append(HTTP_ONLY); + sb.append(COOKIES_FORMAT_SEPARATOR).append(HTTP_ONLY); } - if (cookie.sameSite() != null && !cookie.sameSite().isEmpty()) { - sb.append("; ").append(SAME_SITE).append("=").append(cookie.sameSite()); + if (StringUtils.isNotBlank(cookie.sameSite())) { + sb.append(COOKIES_FORMAT_SEPARATOR) + .append(SAME_SITE) + .append(COOKIE_PAIR_SEPARATOR) + .append(cookie.sameSite()); } return sb.toString(); } @@ -98,54 +116,62 @@ public static Cookie parseSetCookie(String rawCookie) { Cookie.Builder builder = Cookie.builder(); - String[] parts = rawCookie.split(";"); - - String[] nameValue = parts[0].split("=", 2); + String[] parts = rawCookie.split(COOKIES_PARSE_SEPARATOR); + String[] nameValue = parts[0].split(COOKIE_PAIR_SEPARATOR, 2); builder.name(nameValue[0].trim()); - builder.value(nameValue.length > 1 ? nameValue[1].trim() : ""); + builder.value(nameValue.length > 1 ? nameValue[1].trim() : StringUtils.EMPTY); for (int i = 1; i < parts.length; i++) { String part = parts[i].trim(); if (part.isEmpty()) { continue; } - String[] kv = part.split("=", 2); + + String[] kv = part.split(COOKIE_PAIR_SEPARATOR, 2); String key = kv[0].trim().toLowerCase(Locale.ROOT); - String val = kv.length > 1 ? kv[1].trim() : ""; + String val = kv.length > 1 ? kv[1].trim() : StringUtils.EMPTY; - switch (key) { - case "path": - builder.path(val); - break; - case "domain": - builder.domain(val); - break; - case "max-age": - try { - builder.maxAge(Integer.parseInt(val)); - } catch (NumberFormatException ignore) { - } - break; - case "expires": - int maxAge = convertExpiresToMaxAge(val); - builder.maxAge(maxAge); - break; - case "secure": - builder.secure(true); - break; - case "httponly": - builder.httpOnly(true); - break; - case "samesite": - builder.sameSite(val); - break; - default: - break; + if (PATH_KEY.equals(key)) { + builder.path(val); + } else if (DOMAIN_KEY.equals(key)) { + builder.domain(val); + } else if (MAX_AGE_KEY.equals(key)) { + builder.maxAge(safeParseInt(val)); + } else if (EXPIRES_KEY.equals(key)) { + builder.maxAge(convertExpiresToMaxAge(val)); + } else if (SECURE_KEY.equals(key)) { + builder.secure(true); + } else if (HTTP_ONLY_KEY.equals(key)) { + builder.httpOnly(true); + } else if (SAME_SITE_KEY.equals(key)) { + builder.sameSite(val); } } return builder.build(); } + private static int safeParseInt(String val) { + try { + return Integer.parseInt(val); + } catch (NumberFormatException e) { + return -1; + } + } + + private static int convertExpiresToMaxAge(String expiresString) { + if (StringUtils.isBlank(expiresString)) { + return -1; + } + try { + ZonedDateTime expires = + ZonedDateTime.parse(expiresString, DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.US)); + long seconds = Duration.between(ZonedDateTime.now(ZoneOffset.UTC), expires).getSeconds(); + return (int) Math.max(seconds, 0); + } catch (DateTimeParseException e) { + return -1; + } + } + /** * 从 Cookie 头部字符串解析多个 Cookie。 *

示例:{@code "a=1; b=2; c=3"} → List[Cookie(a=1), Cookie(b=2), Cookie(c=3)]

@@ -154,51 +180,32 @@ public static Cookie parseSetCookie(String rawCookie) { * @return 表示解析得到的 Cookie 列表的 {@link List}{@code <}{@link Cookie}{@code >}。 */ public static List parseCookies(String rawCookie) { - if (rawCookie == null || rawCookie.isEmpty()) { + if (StringUtils.isBlank(rawCookie)) { return Collections.emptyList(); } - String[] pairs = rawCookie.split(";"); - List cookies = new ArrayList<>(); - for (String pair : pairs) { + List cookies = new ArrayList<>(); + for (String pair : rawCookie.split(COOKIES_PARSE_SEPARATOR)) { String trimmed = pair.trim(); if (trimmed.isEmpty()) { continue; } - int eqIndex = trimmed.indexOf('='); + int eqIndex = trimmed.indexOf(COOKIE_PAIR_SEPARATOR); if (eqIndex <= 0) { continue; } String name = trimmed.substring(0, eqIndex).trim(); String value = trimmed.substring(eqIndex + 1).trim(); - - if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) { + if (isValueSurrounded(value)) { value = value.substring(1, value.length() - 1); } - cookies.add(Cookie.builder().name(name).value(value).build()); } - return cookies; } - private static int convertExpiresToMaxAge(String expiresString) { - if (StringUtils.isBlank(expiresString)) { - return -1; - } - - try { - ZonedDateTime expires = - ZonedDateTime.parse(expiresString, DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.US)); - long seconds = Duration.between(ZonedDateTime.now(ZoneOffset.UTC), expires).getSeconds(); - return (int) Math.max(seconds, 0); - } catch (DateTimeParseException e) { - return -1; - } - } - /** * 从消息头的字符串值中解析消息头的值。 * From 8672e5d2850f52d896190563cb6b4ef7b0a959d1 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Sat, 11 Oct 2025 09:07:49 +0800 Subject: [PATCH 07/15] [fit] add null and blank name check in CookieCollection#add --- .../modelengine/fit/http/support/DefaultCookieCollection.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index f7ec2463..9f2b100d 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -85,6 +85,9 @@ public int size() { @Override public void add(Cookie cookie) { + if (cookie == null || StringUtils.isBlank(cookie.name())) { + return; + } store.computeIfAbsent(cookie.name(), k -> new ArrayList<>()); List list = store.get(cookie.name()); list.removeIf(c -> From 0a367573ca382daca8fe2a1e21c108121764d536 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Sat, 11 Oct 2025 09:21:20 +0800 Subject: [PATCH 08/15] [fit] safely convert Expires to Max-Age within int range --- .../java/modelengine/fit/http/util/HttpUtils.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index 1d414e7e..f10f564b 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -163,10 +163,18 @@ private static int convertExpiresToMaxAge(String expiresString) { return -1; } try { - ZonedDateTime expires = - ZonedDateTime.parse(expiresString, DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.US)); + ZonedDateTime expires = ZonedDateTime.parse( + expiresString, + DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.US) + ); long seconds = Duration.between(ZonedDateTime.now(ZoneOffset.UTC), expires).getSeconds(); - return (int) Math.max(seconds, 0); + if (seconds <= 0) { + return 0; + } + if (seconds > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) seconds; } catch (DateTimeParseException e) { return -1; } From ab8e415d2452d55f6c77adbbbe17b5cdd4163c52 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Sat, 11 Oct 2025 10:14:03 +0800 Subject: [PATCH 09/15] [fit] check for invalid characters in cookie name and value --- .../modelengine/fit/http/util/HttpUtils.java | 48 ++++++++++++++++--- .../fit/http/util/HttpUtilsTest.java | 19 ++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index f10f564b..a8624c11 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -39,6 +39,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.regex.Pattern; /** * Http 协议相关的工具类。 @@ -54,6 +55,8 @@ public class HttpUtils { private static final String COOKIES_PARSE_SEPARATOR = ";"; private static final String COOKIE_PAIR_SEPARATOR = "="; + private static final Pattern TOKEN_PATTERN = Pattern.compile("^[!#$%&'*+\\-.^_`|~0-9a-zA-Z]+$"); + private static final String PATH_KEY = PATH.toLowerCase(Locale.ROOT); private static final String DOMAIN_KEY = DOMAIN.toLowerCase(Locale.ROOT); private static final String MAX_AGE_KEY = MAX_AGE.toLowerCase(Locale.ROOT); @@ -118,8 +121,19 @@ public static Cookie parseSetCookie(String rawCookie) { String[] parts = rawCookie.split(COOKIES_PARSE_SEPARATOR); String[] nameValue = parts[0].split(COOKIE_PAIR_SEPARATOR, 2); - builder.name(nameValue[0].trim()); - builder.value(nameValue.length > 1 ? nameValue[1].trim() : StringUtils.EMPTY); + + String name = nameValue[0].trim(); + String value = nameValue.length > 1 ? nameValue[1].trim() : StringUtils.EMPTY; + + if (isValueSurrounded(value)) { + value = value.substring(1, value.length() - 1); + } + if (!isValidCookiePair(name, value)) { + return Cookie.builder().build(); + } + + builder.name(name); + builder.value(value); for (int i = 1; i < parts.length; i++) { String part = parts[i].trim(); @@ -163,10 +177,8 @@ private static int convertExpiresToMaxAge(String expiresString) { return -1; } try { - ZonedDateTime expires = ZonedDateTime.parse( - expiresString, - DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.US) - ); + ZonedDateTime expires = + ZonedDateTime.parse(expiresString, DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(Locale.US)); long seconds = Duration.between(ZonedDateTime.now(ZoneOffset.UTC), expires).getSeconds(); if (seconds <= 0) { return 0; @@ -209,11 +221,33 @@ public static List parseCookies(String rawCookie) { if (isValueSurrounded(value)) { value = value.substring(1, value.length() - 1); } - cookies.add(Cookie.builder().name(name).value(value).build()); + if (isValidCookiePair(name, value)) { + cookies.add(Cookie.builder().name(name).value(value).build()); + } } return cookies; } + /** + * 验证给定的 Cookie 名称和值是否合法。 + * + * @param name 表示 Cookie 的名称 {@link String}。 + * @param value 表示 Cookie 的值 {@link String},允许为空但不允许为 {@code null},可带双引号。 + * @return 如果 name 和 value 都合法返回 {@code true},否则返回 {@code false}。 + */ + public static boolean isValidCookiePair(String name, String value) { + if (name == null || name.isEmpty() || !TOKEN_PATTERN.matcher(name).matches()) { + return false; + } + if (value == null) { + return false; + } + if (isValueSurrounded(value)) { + value = value.substring(1, value.length() - 1); + } + return value.isEmpty() || TOKEN_PATTERN.matcher(value).matches(); + } + /** * 从消息头的字符串值中解析消息头的值。 * diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java index 6fa78cab..3641ac7f 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/util/HttpUtilsTest.java @@ -85,6 +85,25 @@ void givenEmptySetCookieThenReturnEmptyCookie() { assertThat(cookie.value()).isNull(); } + @Test + @DisplayName("解析异常或不完整的 Cookie 值时,应忽略非法项并不抛异常") + void givenMalformedCookiesThenHandleGracefully() { + String rawCookie1 = "a=\"incomplete; b=2"; + List cookies1 = HttpUtils.parseCookies(rawCookie1); + assertThat(cookies1).extracting(Cookie::name).contains("b"); + assertThat(cookies1).extracting(Cookie::name).doesNotContain("a"); + + String rawCookie2 = "x=1;; ; y=2;"; + List cookies2 = HttpUtils.parseCookies(rawCookie2); + assertThat(cookies2).hasSize(2); + assertThat(cookies2.get(0).name()).isEqualTo("x"); + assertThat(cookies2.get(1).name()).isEqualTo("y"); + + String rawCookie4 = ";;;"; + List cookies4 = HttpUtils.parseCookies(rawCookie4); + assertThat(cookies4).isEmpty(); + } + @Test @DisplayName("解析带 Expires 属性的 Set-Cookie,自动换算为 Max-Age") void givenExpiresAttributeThenConvertToMaxAge() { From bf6b69e4df106e7d91ef6f0aa6ce791c934c2b38 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Sat, 11 Oct 2025 10:14:43 +0800 Subject: [PATCH 10/15] [fit] validate cookie before adding to collection --- .../http/support/DefaultCookieCollection.java | 3 +++ .../ConfigurableCookieCollectionTest.java | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index 9f2b100d..d694aa56 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -88,6 +88,9 @@ public void add(Cookie cookie) { if (cookie == null || StringUtils.isBlank(cookie.name())) { return; } + if (!HttpUtils.isValidCookiePair(cookie.name(), cookie.value())) { + throw new IllegalArgumentException("Invalid cookie: name or value is not allowed"); + } store.computeIfAbsent(cookie.name(), k -> new ArrayList<>()); List list = store.get(cookie.name()); list.removeIf(c -> diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java index 068670c0..40dc2ca0 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java @@ -54,6 +54,32 @@ void shouldReturnAllCookie() { assertThat(cookies).hasSize(1); } + @Test + @DisplayName("添加非法 Cookie 应抛异常") + void shouldThrowExceptionForInvalidCookie() { + ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(); + + Cookie invalidNameCookie = Cookie.builder().name("inva;lid").value("123").build(); + assertThatThrownBy(() -> collection.add(invalidNameCookie)).isInstanceOf(IllegalArgumentException.class); + + Cookie invalidValueCookie = Cookie.builder().name("validName").value("v@lue;").build(); + assertThatThrownBy(() -> collection.add(invalidValueCookie)).isInstanceOf(IllegalArgumentException.class); + + Cookie nullValueCookie = Cookie.builder().name("someName").value(null).build(); + assertThatThrownBy(() -> collection.add(nullValueCookie)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("允许空字符串 value") + void shouldHandleEmptyAndNullValue() { + ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(); + + // 空字符串 value 是允许的 + Cookie emptyValueCookie = Cookie.builder().name("token").value("").build(); + collection.add(emptyValueCookie); + assertThat(collection.get("token")).isPresent().get().extracting(Cookie::value).isEqualTo(""); + } + @Test @DisplayName("同名 Cookie 不同路径可共存") void shouldAllowMultipleCookiesWithDifferentPath() { From cc07bcc3e27271555ed999f8548cda7278442c5a Mon Sep 17 00:00:00 2001 From: Maiicy Date: Sat, 11 Oct 2025 10:59:22 +0800 Subject: [PATCH 11/15] [fit] solve review comments --- .../client/support/DefaultHttpClassicClientRequest.java | 2 +- .../modelengine/fit/http/header/CookieCollection.java | 6 +++--- .../server/support/DefaultHttpClassicServerResponse.java | 2 +- .../fit/http/support/DefaultCookieCollection.java | 6 +++--- .../main/java/modelengine/fit/http/util/HttpUtils.java | 3 +-- .../fit/http/header/ConfigurableCookieCollectionTest.java | 8 ++++---- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java index 96302f64..10998012 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java @@ -156,7 +156,7 @@ protected void commit() { if (this.isCommitted()) { return; } - this.headers().set(COOKIE, this.cookies().toRequestHeader()); + this.headers().set(COOKIE, this.cookies().toRequestHeaderValue()); super.commit(); } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java index 1df98be1..82a2f457 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java @@ -33,7 +33,7 @@ public interface CookieCollection extends HeaderValue { * @param name 表示 Cookie 名字的 {@link String}。 * @return 返回所有匹配名字的 {@link Cookie} 列表。 */ - List findByName(String name); + List all(String name); /** * 获取集合中所有的 {@link Cookie}。 @@ -55,7 +55,7 @@ public interface CookieCollection extends HeaderValue { * * @return 表示请求头的字符串。 */ - String toRequestHeader(); + String toRequestHeaderValue(); /** * 将集合转换为 HTTP 响应头形式的字符串列表。 @@ -63,5 +63,5 @@ public interface CookieCollection extends HeaderValue { * * @return 表示响应头列表的 {@link List}{@code <}{@link String}{@code >}。 */ - List toResponseHeaders(); + List toResponseHeadersValues(); } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java index f2f65ce6..7849788c 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java @@ -256,7 +256,7 @@ protected void commit() { if (this.isCommitted()) { return; } - this.headers().set(SET_COOKIE, this.cookies().toResponseHeaders()); + this.headers().set(SET_COOKIE, this.cookies().toResponseHeadersValues()); if (this.entity != null) { this.setContentTypeByEntity(this.headers(), this.entity); if (this.entity instanceof FileEntity) { diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index d694aa56..cc7ecd5a 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -63,7 +63,7 @@ public Optional get(String name) { } @Override - public List findByName(String name) { + public List all(String name) { return store.getOrDefault(name, Collections.emptyList()); } @@ -101,14 +101,14 @@ public void add(Cookie cookie) { } @Override - public String toRequestHeader() { + public String toRequestHeaderValue() { return all().stream() .map(c -> c.name() + "=" + c.value()) .collect(Collectors.joining("; ")); } @Override - public List toResponseHeaders() { + public List toResponseHeadersValues() { return all().stream() .map(HttpUtils::formatSetCookie) .collect(Collectors.toList()); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index a8624c11..edef24eb 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -49,10 +49,9 @@ * @since 2022-07-22 */ public class HttpUtils { - private static final char STRING_VALUE_SURROUNDED = '\"'; - private static final String COOKIES_FORMAT_SEPARATOR = "; "; private static final String COOKIES_PARSE_SEPARATOR = ";"; + private static final String COOKIES_FORMAT_SEPARATOR = COOKIES_PARSE_SEPARATOR + " "; private static final String COOKIE_PAIR_SEPARATOR = "="; private static final Pattern TOKEN_PATTERN = Pattern.compile("^[!#$%&'*+\\-.^_`|~0-9a-zA-Z]+$"); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java index 40dc2ca0..48ff20c9 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java @@ -87,7 +87,7 @@ void shouldAllowMultipleCookiesWithDifferentPath() { collection.add(Cookie.builder().name("user").value("A").path("/a").build()); collection.add(Cookie.builder().name("user").value("B").path("/b").build()); - List sameNameCookies = collection.findByName("user"); + List sameNameCookies = collection.all("user"); assertThat(sameNameCookies).hasSize(2); } @@ -102,7 +102,7 @@ void shouldReplaceCookieWithSamePathAndDomain() { collection.add(c1); collection.add(c2); - List cookies = collection.findByName("id"); + List cookies = collection.all("id"); assertThat(cookies).hasSize(1); assertThat(cookies.get(0).value()).isEqualTo("2"); } @@ -114,7 +114,7 @@ void shouldGenerateRequestHeader() { collection.add(Cookie.builder().name("a").value("1").build()); collection.add(Cookie.builder().name("b").value("2").build()); - String header = collection.toRequestHeader(); + String header = collection.toRequestHeaderValue(); assertThat(header).isEqualTo("a=1; b=2"); } @@ -125,7 +125,7 @@ void shouldGenerateMultipleResponseHeaders() { collection.add(Cookie.builder().name("token").value("xyz").secure(true).httpOnly(true).build()); collection.add(Cookie.builder().name("lang").value("zh-CN").sameSite("Lax").build()); - List headers = collection.toResponseHeaders(); + List headers = collection.toResponseHeadersValues(); assertThat(headers).hasSize(2); assertThat(headers.get(0)).contains("token=xyz"); From 3b9ca54d3662929b5a1dac45a38c83dbfffe6ff9 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Sat, 11 Oct 2025 11:31:39 +0800 Subject: [PATCH 12/15] [fit] solve review comments --- .../client/support/DefaultHttpClassicClientRequest.java | 6 ------ .../server/support/DefaultHttpClassicServerResponse.java | 6 ------ .../fit/http/support/AbstractHttpClassicRequest.java | 3 +-- .../fit/http/support/AbstractHttpClassicResponse.java | 3 +-- .../fit/http/support/DefaultCookieCollection.java | 6 ++++-- .../src/main/java/modelengine/fit/http/util/HttpUtils.java | 4 ++-- 6 files changed, 8 insertions(+), 20 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java index 10998012..242a834a 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequest.java @@ -21,7 +21,6 @@ import modelengine.fit.http.entity.ReadableBinaryEntity; import modelengine.fit.http.entity.support.DefaultMultiValueEntity; import modelengine.fit.http.entity.support.DefaultObjectEntity; -import modelengine.fit.http.header.ConfigurableCookieCollection; import modelengine.fit.http.header.ContentType; import modelengine.fit.http.protocol.ClientRequest; import modelengine.fit.http.protocol.ClientResponse; @@ -171,9 +170,4 @@ private void close() { // Ignore } } - - @Override - public ConfigurableCookieCollection cookies() { - return (ConfigurableCookieCollection) super.cookies(); - } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java index 7849788c..ef57de70 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java @@ -26,7 +26,6 @@ import modelengine.fit.http.entity.TextEventStreamEntity; import modelengine.fit.http.entity.WritableBinaryEntity; import modelengine.fit.http.entity.support.DefaultWritableBinaryEntity; -import modelengine.fit.http.header.ConfigurableCookieCollection; import modelengine.fit.http.header.ContentDisposition; import modelengine.fit.http.header.ContentType; import modelengine.fit.http.header.HeaderValue; @@ -281,9 +280,4 @@ private void close0() throws IOException { this.entity = null; } } - - @Override - public ConfigurableCookieCollection cookies() { - return (ConfigurableCookieCollection) super.cookies(); - } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java index 7371e989..497c6edd 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java @@ -13,7 +13,6 @@ import modelengine.fit.http.HttpClassicRequest; import modelengine.fit.http.HttpResource; import modelengine.fit.http.header.ConfigurableCookieCollection; -import modelengine.fit.http.header.CookieCollection; import modelengine.fit.http.header.HeaderValue; import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.MessageHeaderNames; @@ -77,7 +76,7 @@ public QueryCollection queries() { } @Override - public CookieCollection cookies() { + public ConfigurableCookieCollection cookies() { return this.cookies; } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java index bf1d54b3..81f63a42 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java @@ -12,7 +12,6 @@ import modelengine.fit.http.HttpClassicResponse; import modelengine.fit.http.HttpResource; import modelengine.fit.http.header.ConfigurableCookieCollection; -import modelengine.fit.http.header.CookieCollection; import modelengine.fit.http.protocol.MessageHeaders; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.StatusLine; @@ -57,7 +56,7 @@ public String reasonPhrase() { } @Override - public CookieCollection cookies() { + public ConfigurableCookieCollection cookies() { return this.cookies; } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index cc7ecd5a..364c4804 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -6,6 +6,8 @@ package modelengine.fit.http.support; +import static modelengine.fit.http.util.HttpUtils.COOKIES_FORMAT_SEPARATOR; +import static modelengine.fit.http.util.HttpUtils.COOKIE_PAIR_SEPARATOR; import static modelengine.fitframework.inspection.Validation.notNull; import modelengine.fit.http.Cookie; @@ -103,8 +105,8 @@ public void add(Cookie cookie) { @Override public String toRequestHeaderValue() { return all().stream() - .map(c -> c.name() + "=" + c.value()) - .collect(Collectors.joining("; ")); + .map(c -> c.name() + COOKIE_PAIR_SEPARATOR + c.value()) + .collect(Collectors.joining(COOKIES_FORMAT_SEPARATOR)); } @Override diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index edef24eb..a47e561b 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -51,8 +51,8 @@ public class HttpUtils { private static final char STRING_VALUE_SURROUNDED = '\"'; private static final String COOKIES_PARSE_SEPARATOR = ";"; - private static final String COOKIES_FORMAT_SEPARATOR = COOKIES_PARSE_SEPARATOR + " "; - private static final String COOKIE_PAIR_SEPARATOR = "="; + public static final String COOKIES_FORMAT_SEPARATOR = COOKIES_PARSE_SEPARATOR + " "; + public static final String COOKIE_PAIR_SEPARATOR = "="; private static final Pattern TOKEN_PATTERN = Pattern.compile("^[!#$%&'*+\\-.^_`|~0-9a-zA-Z]+$"); From 4f1a0683b60a1a7baed99a0114bab14dc55c34ec Mon Sep 17 00:00:00 2001 From: Maiicy Date: Mon, 13 Oct 2025 10:02:38 +0800 Subject: [PATCH 13/15] [fit] resolve review comments - Add notes for deprecated fields `comment` and `version` - Split long Set-Cookie parsing method into smaller parts - Remove unnecessary variable `header1` --- .../java/modelengine/fit/http/Cookie.java | 15 +++ .../support/AbstractHttpClassicResponse.java | 4 +- .../http/support/DefaultCookieCollection.java | 2 +- .../modelengine/fit/http/util/HttpUtils.java | 119 +++++++++--------- 4 files changed, 78 insertions(+), 62 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java index ffc54c09..8721b7d5 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/Cookie.java @@ -117,6 +117,11 @@ interface Builder { /** * 向当前构建器中设置 Cookie 的版本。 + *

+ * 此属性源自 RFC 2965, + * 但已在 RFC 6265 + * 中移出标准定义。现代浏览器会忽略该属性。 + *

* * @param version 表示待设置的 Cookie 版本的 {@code int}。 * @return 表示当前构建器的 {@link Builder}。 @@ -126,6 +131,11 @@ interface Builder { /** * 向当前构建器中设置 Cookie 的注释。 + *

+ * 此属性源自 RFC 2965, + * 但已在 RFC 6265 + * 中移出标准定义。现代浏览器会忽略该属性。 + *

* * @param comment 表示待设置的 Cookie 注释的 {@link String}。 * @return 表示当前构建器的 {@link Builder}。 @@ -175,6 +185,11 @@ interface Builder { /** * 向当前构建器中设置 Cookie 限制跨站请求时发送行为安全级别。 + *

+ * 该属性定义于 + * RFC 6265bis 草案第 4.1.2.7 节,用于控制跨站请求时是否发送 Cookie。 + * 尽管该规范尚处于草案阶段,但已被主流浏览器(如 Chrome、Firefox、Safari、Edge)广泛支持。 + *

* * @param sameSite SameSite 值,如 "Strict", "Lax", "None"。 * @return 表示当前构建器的 {@link Builder}。 diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java index 81f63a42..f30888e7 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java @@ -39,8 +39,8 @@ public abstract class AbstractHttpClassicResponse extends AbstractHttpMessage im public AbstractHttpClassicResponse(HttpResource httpResource, StatusLine startLine, MessageHeaders headers) { super(httpResource, startLine, headers); this.startLine = notNull(startLine, "The status line cannot be null."); - MessageHeaders headers1 = notNull(headers, "The headers cannot be null."); - List actualCookies = headers1.all(SET_COOKIE); + notNull(headers, "The headers cannot be null."); + List actualCookies = headers.all(SET_COOKIE); this.cookies = ConfigurableCookieCollection.create(); actualCookies.stream().map(HttpUtils::parseSetCookie).forEach(cookies::add); } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index 364c4804..06427ee7 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -90,7 +90,7 @@ public void add(Cookie cookie) { if (cookie == null || StringUtils.isBlank(cookie.name())) { return; } - if (!HttpUtils.isValidCookiePair(cookie.name(), cookie.value())) { + if (HttpUtils.isInvalidCookiePair(cookie.name(), cookie.value())) { throw new IllegalArgumentException("Invalid cookie: name or value is not allowed"); } store.computeIfAbsent(cookie.name(), k -> new ArrayList<>()); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index a47e561b..7901395b 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -116,50 +116,13 @@ public static Cookie parseSetCookie(String rawCookie) { return Cookie.builder().build(); } - Cookie.Builder builder = Cookie.builder(); - - String[] parts = rawCookie.split(COOKIES_PARSE_SEPARATOR); - String[] nameValue = parts[0].split(COOKIE_PAIR_SEPARATOR, 2); - - String name = nameValue[0].trim(); - String value = nameValue.length > 1 ? nameValue[1].trim() : StringUtils.EMPTY; - - if (isValueSurrounded(value)) { - value = value.substring(1, value.length() - 1); - } - if (!isValidCookiePair(name, value)) { + var parts = rawCookie.split(COOKIES_PARSE_SEPARATOR); + var builder = parseCookieNameValue(parts[0]); + if (builder == null) { return Cookie.builder().build(); } - builder.name(name); - builder.value(value); - - for (int i = 1; i < parts.length; i++) { - String part = parts[i].trim(); - if (part.isEmpty()) { - continue; - } - - String[] kv = part.split(COOKIE_PAIR_SEPARATOR, 2); - String key = kv[0].trim().toLowerCase(Locale.ROOT); - String val = kv.length > 1 ? kv[1].trim() : StringUtils.EMPTY; - - if (PATH_KEY.equals(key)) { - builder.path(val); - } else if (DOMAIN_KEY.equals(key)) { - builder.domain(val); - } else if (MAX_AGE_KEY.equals(key)) { - builder.maxAge(safeParseInt(val)); - } else if (EXPIRES_KEY.equals(key)) { - builder.maxAge(convertExpiresToMaxAge(val)); - } else if (SECURE_KEY.equals(key)) { - builder.secure(true); - } else if (HTTP_ONLY_KEY.equals(key)) { - builder.httpOnly(true); - } else if (SAME_SITE_KEY.equals(key)) { - builder.sameSite(val); - } - } + parseCookieAttributes(parts, builder); return builder.build(); } @@ -204,27 +167,65 @@ public static List parseCookies(String rawCookie) { } List cookies = new ArrayList<>(); - for (String pair : rawCookie.split(COOKIES_PARSE_SEPARATOR)) { - String trimmed = pair.trim(); - if (trimmed.isEmpty()) { - continue; + for (String part : rawCookie.split(COOKIES_PARSE_SEPARATOR)) { + Cookie.Builder builder = parseCookieNameValue(part.trim()); + if (builder != null) { + cookies.add(builder.build()); } + } + return cookies; + } + + private static Cookie.Builder parseCookieNameValue(String part) { + String trimmed = part.trim(); + if (trimmed.isEmpty()) { + return null; + } + + int eqIndex = trimmed.indexOf(COOKIE_PAIR_SEPARATOR); + if (eqIndex <= 0) { + return null; + } + + String name = trimmed.substring(0, eqIndex).trim(); + String value = trimmed.substring(eqIndex + 1).trim(); + if (isValueSurrounded(value)) { + value = value.substring(1, value.length() - 1); + } + + if (isInvalidCookiePair(name, value)) { + return null; + } + return Cookie.builder().name(name).value(value); + } - int eqIndex = trimmed.indexOf(COOKIE_PAIR_SEPARATOR); - if (eqIndex <= 0) { + private static void parseCookieAttributes(String[] parts, Cookie.Builder builder) { + for (int i = 1; i < parts.length; i++) { + var part = parts[i].trim(); + if (part.isEmpty()) { continue; } - String name = trimmed.substring(0, eqIndex).trim(); - String value = trimmed.substring(eqIndex + 1).trim(); - if (isValueSurrounded(value)) { - value = value.substring(1, value.length() - 1); - } - if (isValidCookiePair(name, value)) { - cookies.add(Cookie.builder().name(name).value(value).build()); + var kv = part.split(COOKIE_PAIR_SEPARATOR, 2); + var key = kv[0].trim().toLowerCase(Locale.ROOT); + var val = kv.length > 1 ? kv[1].trim() : StringUtils.EMPTY; + + if (PATH_KEY.equals(key)) { + builder.path(val); + } else if (DOMAIN_KEY.equals(key)) { + builder.domain(val); + } else if (MAX_AGE_KEY.equals(key)) { + builder.maxAge(safeParseInt(val)); + } else if (EXPIRES_KEY.equals(key)) { + builder.maxAge(convertExpiresToMaxAge(val)); + } else if (SECURE_KEY.equals(key)) { + builder.secure(true); + } else if (HTTP_ONLY_KEY.equals(key)) { + builder.httpOnly(true); + } else if (SAME_SITE_KEY.equals(key)) { + builder.sameSite(val); } } - return cookies; } /** @@ -234,17 +235,17 @@ public static List parseCookies(String rawCookie) { * @param value 表示 Cookie 的值 {@link String},允许为空但不允许为 {@code null},可带双引号。 * @return 如果 name 和 value 都合法返回 {@code true},否则返回 {@code false}。 */ - public static boolean isValidCookiePair(String name, String value) { + public static boolean isInvalidCookiePair(String name, String value) { if (name == null || name.isEmpty() || !TOKEN_PATTERN.matcher(name).matches()) { - return false; + return true; } if (value == null) { - return false; + return true; } if (isValueSurrounded(value)) { value = value.substring(1, value.length() - 1); } - return value.isEmpty() || TOKEN_PATTERN.matcher(value).matches(); + return !value.isEmpty() && !TOKEN_PATTERN.matcher(value).matches(); } /** From 9b73d4317c5e6f86bc41fbe3d3e9372315dde573 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Mon, 13 Oct 2025 16:38:43 +0800 Subject: [PATCH 14/15] [fit] refactor structure & opt extend --- .../header/ConfigurableCookieCollection.java | 11 ----- .../fit/http/header/CookieCollection.java | 2 +- .../support/AbstractHttpClassicRequest.java | 11 +---- .../support/AbstractHttpClassicResponse.java | 10 +---- .../fit/http/support/AbstractHttpMessage.java | 8 +++- .../http/support/DefaultCookieCollection.java | 43 +++---------------- .../ConfigurableCookieCollectionTest.java | 23 ---------- 7 files changed, 16 insertions(+), 92 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/ConfigurableCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/ConfigurableCookieCollection.java index 87cb3012..0fcf37f1 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/ConfigurableCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/ConfigurableCookieCollection.java @@ -31,15 +31,4 @@ public interface ConfigurableCookieCollection extends CookieCollection { static ConfigurableCookieCollection create() { return new DefaultCookieCollection(); } - - /** - * 根据指定的消息头创建一个可读可写的 Cookie 集合。 - * - * @param headerValue 表示指定消息头的 {@link HeaderValue}。 - * @return 表示创建出来的可读可写的 Cookie 集合的 {@link ConfigurableCookieCollection}。 - * @throws IllegalArgumentException 当 {@code headerValue} 为 {@code null} 时。 - */ - static ConfigurableCookieCollection create(HeaderValue headerValue) { - return new DefaultCookieCollection(headerValue); - } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java index 82a2f457..23b07c51 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/header/CookieCollection.java @@ -17,7 +17,7 @@ * @author 季聿阶 * @since 2022-07-06 */ -public interface CookieCollection extends HeaderValue { +public interface CookieCollection { /** * 获取指定名字的 {@link Cookie}。 *

如果存在多个同名 Cookie,返回第一个匹配的 Cookie。

diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java index 497c6edd..ccfc79ba 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java @@ -12,13 +12,12 @@ import modelengine.fit.http.HttpClassicRequest; import modelengine.fit.http.HttpResource; -import modelengine.fit.http.header.ConfigurableCookieCollection; -import modelengine.fit.http.header.HeaderValue; import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.MessageHeaderNames; import modelengine.fit.http.protocol.MessageHeaders; import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; +import modelengine.fit.http.util.HttpUtils; /** * 表示 {@link HttpClassicRequest} 的抽象实现类。 @@ -31,7 +30,6 @@ public abstract class AbstractHttpClassicRequest extends AbstractHttpMessage imp private final RequestLine startLine; private final MessageHeaders headers; - private final ConfigurableCookieCollection cookies; /** * 创建经典的 Http 请求对象。 @@ -45,7 +43,7 @@ public AbstractHttpClassicRequest(HttpResource httpResource, RequestLine startLi this.startLine = notNull(startLine, "The request line cannot be null."); this.headers = notNull(headers, "The message headers cannot be null."); String actualCookie = String.join(COOKIE_DELIMITER, this.headers.all(COOKIE)); - this.cookies = ConfigurableCookieCollection.create(HeaderValue.create(actualCookie)); + HttpUtils.parseCookies(actualCookie).forEach(this.cookies()::add); } @Override @@ -74,9 +72,4 @@ public String path() { public QueryCollection queries() { return this.startLine.queries(); } - - @Override - public ConfigurableCookieCollection cookies() { - return this.cookies; - } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java index f30888e7..7f6794a7 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicResponse.java @@ -11,7 +11,6 @@ import modelengine.fit.http.HttpClassicResponse; import modelengine.fit.http.HttpResource; -import modelengine.fit.http.header.ConfigurableCookieCollection; import modelengine.fit.http.protocol.MessageHeaders; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.StatusLine; @@ -27,7 +26,6 @@ */ public abstract class AbstractHttpClassicResponse extends AbstractHttpMessage implements HttpClassicResponse { private final StatusLine startLine; - private final ConfigurableCookieCollection cookies; /** * 创建经典的 Http 响应对象。 @@ -41,8 +39,7 @@ public AbstractHttpClassicResponse(HttpResource httpResource, StatusLine startLi this.startLine = notNull(startLine, "The status line cannot be null."); notNull(headers, "The headers cannot be null."); List actualCookies = headers.all(SET_COOKIE); - this.cookies = ConfigurableCookieCollection.create(); - actualCookies.stream().map(HttpUtils::parseSetCookie).forEach(cookies::add); + actualCookies.stream().map(HttpUtils::parseSetCookie).forEach(this.cookies()::add); } @Override @@ -54,9 +51,4 @@ public int statusCode() { public String reasonPhrase() { return this.startLine.reasonPhrase(); } - - @Override - public ConfigurableCookieCollection cookies() { - return this.cookies; - } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java index 668f7fad..9ab1d362 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpMessage.java @@ -8,7 +8,6 @@ import static modelengine.fit.http.protocol.MessageHeaderNames.CONTENT_LENGTH; import static modelengine.fit.http.protocol.MessageHeaderNames.CONTENT_TYPE; -import static modelengine.fit.http.protocol.MessageHeaderNames.COOKIE; import static modelengine.fit.http.protocol.MessageHeaderNames.TRANSFER_ENCODING; import static modelengine.fit.http.protocol.MessageHeaderValues.CHUNKED; import static modelengine.fitframework.inspection.Validation.notNull; @@ -52,6 +51,7 @@ public abstract class AbstractHttpMessage implements HttpMessage { private final HttpResource httpResource; private final StartLine startLine; private final MessageHeaders headers; + private final ConfigurableCookieCollection cookies; private final Map> customEntitySerializers = new HashMap<>(); private ObjectSerializer customJsonSerializer; private boolean isCommitted; @@ -67,6 +67,7 @@ protected AbstractHttpMessage(HttpResource httpResource, StartLine startLine, Me this.httpResource = notNull(httpResource, "The http resource cannot be null."); this.startLine = notNull(startLine, "The start line cannot be null."); this.headers = notNull(headers, "The message headers cannot be null."); + this.cookies = ConfigurableCookieCollection.create(); } @Override @@ -135,6 +136,11 @@ public int contentLength() { return Integer.parseInt(value); } + @Override + public ConfigurableCookieCollection cookies() { + return this.cookies; + } + /** * 提交当前的 Http 消息。 *

提交之后的 Http 消息将无法修改。

diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index 06427ee7..68d9cf69 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -8,14 +8,10 @@ import static modelengine.fit.http.util.HttpUtils.COOKIES_FORMAT_SEPARATOR; import static modelengine.fit.http.util.HttpUtils.COOKIE_PAIR_SEPARATOR; -import static modelengine.fitframework.inspection.Validation.notNull; import modelengine.fit.http.Cookie; import modelengine.fit.http.header.ConfigurableCookieCollection; import modelengine.fit.http.header.CookieCollection; -import modelengine.fit.http.header.HeaderValue; -import modelengine.fit.http.header.support.DefaultHeaderValue; -import modelengine.fit.http.header.support.DefaultParameterCollection; import modelengine.fit.http.util.HttpUtils; import modelengine.fitframework.util.StringUtils; @@ -34,27 +30,9 @@ * @author 季聿阶 * @since 2022-07-06 */ -public class DefaultCookieCollection extends DefaultHeaderValue implements ConfigurableCookieCollection { +public class DefaultCookieCollection implements ConfigurableCookieCollection { private final Map> store = new LinkedHashMap<>(); - /** - * 初始化 {@link DefaultCookieCollection} 的新实例。 - */ - public DefaultCookieCollection() { - super(StringUtils.EMPTY, new DefaultParameterCollection()); - } - - /** - * 使用指定的消息头初始化 {@link DefaultCookieCollection} 的新实例。 - * - * @param headerValue 表示消息头的 {@link HeaderValue}。 - * @throws IllegalArgumentException 当 {@code headerValue} 为 {@code null} 时。 - */ - public DefaultCookieCollection(HeaderValue headerValue) { - super(notNull(headerValue, "The header value cannot be null.").value(), headerValue.parameters()); - HttpUtils.parseCookies(headerValue.value()).forEach(this::add); - } - @Override public Optional get(String name) { List cookies = store.get(name); @@ -71,18 +49,12 @@ public List all(String name) { @Override public List all() { - return store.values() - .stream() - .flatMap(List::stream) - .collect(Collectors.toList()); + return store.values().stream().flatMap(List::stream).collect(Collectors.toList()); } @Override public int size() { - return store.values() - .stream() - .mapToInt(List::size) - .sum(); + return store.values().stream().mapToInt(List::size).sum(); } @Override @@ -95,10 +67,7 @@ public void add(Cookie cookie) { } store.computeIfAbsent(cookie.name(), k -> new ArrayList<>()); List list = store.get(cookie.name()); - list.removeIf(c -> - Objects.equals(c.path(), cookie.path()) && - Objects.equals(c.domain(), cookie.domain()) - ); + list.removeIf(c -> Objects.equals(c.path(), cookie.path()) && Objects.equals(c.domain(), cookie.domain())); list.add(cookie); } @@ -111,8 +80,6 @@ public String toRequestHeaderValue() { @Override public List toResponseHeadersValues() { - return all().stream() - .map(HttpUtils::formatSetCookie) - .collect(Collectors.toList()); + return all().stream().map(HttpUtils::formatSetCookie).collect(Collectors.toList()); } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java index 48ff20c9..c5219fe5 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/header/ConfigurableCookieCollectionTest.java @@ -24,24 +24,12 @@ */ @DisplayName("测试 ConfigurableCookieCollection 类") class ConfigurableCookieCollectionTest { - @Test - @DisplayName("当构建 Cookie 集合的参数为 null 时,抛出异常") - void givenNullThenThrowException() { - assertThatThrownBy(this::buildNullCookieCollection).isInstanceOf(IllegalArgumentException.class); - } - - private void buildNullCookieCollection() { - ConfigurableCookieCollection.create(null); - } - @Test @DisplayName("返回所有的 Cookie") void shouldReturnAllCookie() { final Cookie cookie = Cookie.builder() .name("idea") .value("00ae-u98i") - .version(1) - .comment("") .domain("localhost") .maxAge(10) .path("/") @@ -132,15 +120,4 @@ void shouldGenerateMultipleResponseHeaders() { assertThat(headers.get(1)).contains("lang=zh-CN"); assertThat(headers.get(1)).contains("SameSite=Lax"); } - - @Test - @DisplayName("从 HeaderValue 初始化应正确解析多个 Cookie") - void shouldInitializeFromHeaderValue() { - String header = "a=1; b=2; c=3"; - ConfigurableCookieCollection collection = ConfigurableCookieCollection.create(HeaderValue.create(header)); - - assertThat(collection.size()).isEqualTo(3); - assertThat(collection.get("b")).isPresent(); - assertThat(collection.get("b").get().value()).isEqualTo("2"); - } } From e34fd8bdeced1c7ea832894543e1459b5cd5e741 Mon Sep 17 00:00:00 2001 From: Maiicy Date: Mon, 13 Oct 2025 19:43:02 +0800 Subject: [PATCH 15/15] [fit] solve review comments --- .../http/support/DefaultCookieCollection.java | 17 +++++++++-------- .../modelengine/fit/http/util/HttpUtils.java | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java index 68d9cf69..dee4b65d 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultCookieCollection.java @@ -13,6 +13,7 @@ import modelengine.fit.http.header.ConfigurableCookieCollection; import modelengine.fit.http.header.CookieCollection; import modelengine.fit.http.util.HttpUtils; +import modelengine.fitframework.util.CollectionUtils; import modelengine.fitframework.util.StringUtils; import java.util.ArrayList; @@ -35,8 +36,8 @@ public class DefaultCookieCollection implements ConfigurableCookieCollection { @Override public Optional get(String name) { - List cookies = store.get(name); - if (cookies == null || cookies.isEmpty()) { + List cookies = this.store.get(name); + if (CollectionUtils.isEmpty(cookies)) { return Optional.empty(); } return Optional.of(cookies.get(0)); @@ -44,17 +45,17 @@ public Optional get(String name) { @Override public List all(String name) { - return store.getOrDefault(name, Collections.emptyList()); + return this.store.getOrDefault(name, Collections.emptyList()); } @Override public List all() { - return store.values().stream().flatMap(List::stream).collect(Collectors.toList()); + return this.store.values().stream().flatMap(List::stream).collect(Collectors.toList()); } @Override public int size() { - return store.values().stream().mapToInt(List::size).sum(); + return this.store.values().stream().mapToInt(List::size).sum(); } @Override @@ -63,10 +64,10 @@ public void add(Cookie cookie) { return; } if (HttpUtils.isInvalidCookiePair(cookie.name(), cookie.value())) { - throw new IllegalArgumentException("Invalid cookie: name or value is not allowed"); + throw new IllegalArgumentException("Invalid cookie: name or value is not allowed."); } - store.computeIfAbsent(cookie.name(), k -> new ArrayList<>()); - List list = store.get(cookie.name()); + this.store.computeIfAbsent(cookie.name(), k -> new ArrayList<>()); + List list = this.store.get(cookie.name()); list.removeIf(c -> Objects.equals(c.path(), cookie.path()) && Objects.equals(c.domain(), cookie.domain())); list.add(cookie); } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index 7901395b..4aa4df42 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -236,7 +236,7 @@ private static void parseCookieAttributes(String[] parts, Cookie.Builder builder * @return 如果 name 和 value 都合法返回 {@code true},否则返回 {@code false}。 */ public static boolean isInvalidCookiePair(String name, String value) { - if (name == null || name.isEmpty() || !TOKEN_PATTERN.matcher(name).matches()) { + if (StringUtils.isEmpty(name) || !TOKEN_PATTERN.matcher(name).matches()) { return true; } if (value == null) {