Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public interface Cookie {
*
* @return 表示 Cookie 版本号的 {@code int}。
*/
@Deprecated
int version();

/**
Expand All @@ -43,6 +44,7 @@ public interface Cookie {
*
* @return 表示 Cookie 注释的 {@link String}。
*/
@Deprecated
String comment();

/**
Expand Down Expand Up @@ -79,12 +81,20 @@ public interface Cookie {

/**
* 判断 Cookie 是否仅允许在服务端获取。
* <p>该属性并不是 Cookie 的标准,但是被浏览器支持。</p>
* <p>其 HttpOnly 属性的格式为 {@code ;HttpOnly ...},如果存在则表示仅服务端可访问。</p>
*
* @return 如果 Cookie 仅允许在服务端获取,返回 {@code true},否则,返回 {@code false}。
* @return 如果 Cookie 仅允许在服务端访问,则返回 {@code true},否则返回 {@code false}。
*/
boolean httpOnly();

/**
* 获取 Cookie 的 SameSite 属性。
* <p>其 SameSite 属性的格式为 {@code ;SameSite=VALUE ...},表示跨站请求策略。</p>
*
* @return SameSite 值,如 {@code "Strict"}、{@code "Lax"}、{@code "None"}。
*/
String sameSite();

/**
* {@link Cookie} 的构建器。
*/
Expand All @@ -107,18 +117,30 @@ interface Builder {

/**
* 向当前构建器中设置 Cookie 的版本。
* <p>
* 此属性源自 <a href="https://datatracker.ietf.org/doc/html/rfc2965">RFC 2965</a>,
* 但已在 <a href="https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2">RFC 6265</a>
* 中移出标准定义。现代浏览器会忽略该属性。
* </p>
*
* @param version 表示待设置的 Cookie 版本的 {@code int}。
* @return 表示当前构建器的 {@link Builder}。
*/
@Deprecated
Builder version(int version);

/**
* 向当前构建器中设置 Cookie 的注释。
* <p>
* 此属性源自 <a href="https://datatracker.ietf.org/doc/html/rfc2965">RFC 2965</a>,
* 但已在 <a href="https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2">RFC 6265</a>
* 中移出标准定义。现代浏览器会忽略该属性。
* </p>
*
* @param comment 表示待设置的 Cookie 注释的 {@link String}。
* @return 表示当前构建器的 {@link Builder}。
*/
@Deprecated
Builder comment(String comment);

/**
Expand Down Expand Up @@ -161,6 +183,19 @@ interface Builder {
*/
Builder httpOnly(boolean httpOnly);

/**
* 向当前构建器中设置 Cookie 限制跨站请求时发送行为安全级别。
* <p>
* 该属性定义于 <a href="https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">
* RFC 6265bis 草案第 4.1.2.7 节</a>,用于控制跨站请求时是否发送 Cookie。
* 尽管该规范尚处于草案阶段,但已被主流浏览器(如 Chrome、Firefox、Safari、Edge)广泛支持。
* </p>
*
* @param sameSite SameSite 值,如 "Strict", "Lax", "None"。
* @return 表示当前构建器的 {@link Builder}。
*/
Builder sameSite(String sameSite);

/**
* 构建对象。
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ protected void commit() {
if (this.isCommitted()) {
return;
}
this.headers().set(COOKIE, this.cookies().toString());
this.headers().set(COOKIE, this.cookies().toRequestHeaderValue());
super.commit();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,26 @@
* @author 季聿阶
* @since 2022-07-06
*/
public interface CookieCollection extends HeaderValue {
public interface CookieCollection {
/**
* 获取指定名字的 {@link Cookie}。
* <p>如果存在多个同名 Cookie,返回第一个匹配的 Cookie。</p>
*
* @param name 表示 Cookie 名字的 {@link Optional}{@code <}{@link String}{@code >}。
* @return 表示指定名字的 {@link Cookie}。
*/
Optional<Cookie> get(String name);

/**
* 获取所有的 {@link Cookie}。
* 根据名字查找所有匹配的 {@link Cookie}。
*
* @param name 表示 Cookie 名字的 {@link String}。
* @return 返回所有匹配名字的 {@link Cookie} 列表。
*/
List<Cookie> all(String name);

/**
* 获取集合中所有的 {@link Cookie}。
*
* @return 表示所有 {@link Cookie} 列表的 {@link List}{@code <}{@link Cookie}{@code >}。
*/
Expand All @@ -39,4 +48,20 @@ public interface CookieCollection extends HeaderValue {
* @return 表示所有 {@link Cookie} 的数量的 {@code int}。
*/
int size();

/**
* 将集合转换为 HTTP 请求头中 Cookie 形式的字符串。
* <p>格式为 {@code name1=value1; name2=value2; ...}。</p>
*
* @return 表示请求头的字符串。
*/
String toRequestHeaderValue();

/**
* 将集合转换为 HTTP 响应头形式的字符串列表。
* <p>每个 Cookie 对应一个 {@code Set-Cookie: ...} 头。</p>
*
* @return 表示响应头列表的 {@link List}{@code <}{@link String}{@code >}。
*/
List<String> toResponseHeadersValues();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -255,7 +255,7 @@ protected void commit() {
if (this.isCommitted()) {
return;
}
this.headers().set(COOKIE, this.cookies().toString());
this.headers().set(SET_COOKIE, this.cookies().toResponseHeadersValues());
if (this.entity != null) {
this.setContentTypeByEntity(this.headers(), this.entity);
if (this.entity instanceof FileEntity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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;

Expand All @@ -16,6 +17,7 @@
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} 的抽象实现类。
Expand All @@ -24,6 +26,8 @@
* @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;

Expand All @@ -38,6 +42,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));
HttpUtils.parseCookies(actualCookie).forEach(this.cookies()::add);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@

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.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} 的默认实现。
Expand All @@ -33,6 +37,9 @@ 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.");
notNull(headers, "The headers cannot be null.");
List<String> actualCookies = headers.all(SET_COOKIE);
actualCookies.stream().map(HttpUtils::parseSetCookie).forEach(this.cookies()::add);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,8 +46,6 @@
* @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;
Expand All @@ -70,8 +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.");
String actualCookie = String.join(COOKIE_DELIMITER, this.headers.all(COOKIE));
this.cookies = ConfigurableCookieCollection.create(HttpUtils.parseHeaderValue(actualCookie));
this.cookies = ConfigurableCookieCollection.create();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@

package modelengine.fit.http.support;

import static modelengine.fitframework.inspection.Validation.notNull;
import static modelengine.fit.http.util.HttpUtils.COOKIES_FORMAT_SEPARATOR;
import static modelengine.fit.http.util.HttpUtils.COOKIE_PAIR_SEPARATOR;

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.CollectionUtils;
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;

Expand All @@ -27,49 +31,56 @@
* @author 季聿阶
* @since 2022-07-06
*/
public class DefaultCookieCollection extends DefaultHeaderValue implements ConfigurableCookieCollection {
/**
* 初始化 {@link DefaultCookieCollection} 的新实例。
*/
public DefaultCookieCollection() {
super(StringUtils.EMPTY, new DefaultParameterCollection());
}
public class DefaultCookieCollection implements ConfigurableCookieCollection {
private final Map<String, List<Cookie>> store = new LinkedHashMap<>();

/**
* 使用指定的消息头初始化 {@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());
@Override
public Optional<Cookie> get(String name) {
List<Cookie> cookies = this.store.get(name);
if (CollectionUtils.isEmpty(cookies)) {
return Optional.empty();
}
return Optional.of(cookies.get(0));
}

@Override
public Optional<Cookie> get(String name) {
return this.parameters().get(name).map(value -> Cookie.builder().name(name).value(value).build());
public List<Cookie> all(String name) {
return this.store.getOrDefault(name, Collections.emptyList());
}

@Override
public List<Cookie> all() {
return Collections.unmodifiableList(this.parameters()
.keys()
.stream()
.map(this::get)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList()));
return this.store.values().stream().flatMap(List::stream).collect(Collectors.toList());
}

@Override
public int size() {
return this.parameters().size();
return this.store.values().stream().mapToInt(List::size).sum();
}

@Override
public void add(Cookie cookie) {
if (cookie != null) {
this.parameters().set(cookie.name(), cookie.value());
if (cookie == null || StringUtils.isBlank(cookie.name())) {
return;
}
if (HttpUtils.isInvalidCookiePair(cookie.name(), cookie.value())) {
throw new IllegalArgumentException("Invalid cookie: name or value is not allowed.");
}
this.store.computeIfAbsent(cookie.name(), k -> new ArrayList<>());
List<Cookie> list = this.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 toRequestHeaderValue() {
return all().stream()
.map(c -> c.name() + COOKIE_PAIR_SEPARATOR + c.value())
.collect(Collectors.joining(COOKIES_FORMAT_SEPARATOR));
}

@Override
public List<String> toResponseHeadersValues() {
return all().stream().map(HttpUtils::formatSetCookie).collect(Collectors.toList());
}
}
Loading