diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/Cookies.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/Cookies.java index 2bc4e39155..009e6fc637 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/Cookies.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/Cookies.java @@ -15,93 +15,34 @@ */ package reactor.netty5.http; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import io.netty5.handler.codec.http.headers.HttpCookiePair; -import io.netty5.handler.codec.http.headers.HttpHeaders; -import io.netty5.handler.codec.http.headers.HttpSetCookie; -import reactor.util.Logger; -import reactor.util.Loggers; /** - * Holder for Set-Cookie headers found from response headers + * Base holder for Set-Cookie/Cookie headers found in response headers/request headers * * @since 0.6 */ -public class Cookies { - - /** - * Return a new cookies holder from client response headers. - * - * @param headers client response headers - * @return a new cookies holder from client response headers - */ - public static Cookies newClientResponseHolder(HttpHeaders headers) { - return new Cookies(headers); - } +public abstract class Cookies { final static int NOT_READ = 0; final static int READING = 1; final static int READ = 2; - protected final HttpHeaders nettyHeaders; - - protected Map> cachedCookies; - volatile int state; + @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater STATE = AtomicIntegerFieldUpdater.newUpdater(Cookies.class, "state"); - static final Logger log = Loggers.getLogger(Cookies.class); - - protected Cookies(HttpHeaders nettyHeaders) { - this.nettyHeaders = Objects.requireNonNull(nettyHeaders, "nettyHeaders"); - cachedCookies = Collections.emptyMap(); - } /** * Wait for the cookies to become available, cache them and subsequently return the cached map of cookies. * * @return the cached map of cookies */ - public Map> getCachedCookies() { - if (!markReadingCookies()) { - for (;;) { - if (hasReadCookies()) { - return cachedCookies; - } - } - } - - Map> cookies = new HashMap<>(); - Iterator setCookieItr = nettyHeaders.getSetCookiesIterator(); - int setCookieIndex = -1; - while (setCookieItr.hasNext()) { - try { - setCookieIndex++; - HttpSetCookie setCookie = setCookieItr.next(); - Set existingCookiesOfName = cookies.computeIfAbsent(setCookie.name(), k -> new HashSet<>()); - existingCookiesOfName.add(setCookie); - } - catch (IllegalArgumentException err) { - // Ignore invalid syntax or whatever decoding error for the current Set-Cookie header. - // Since we don't know which Set-Cookie header is not parsed, we log the Set-Cookie header index number. - if (log.isDebugEnabled()) { - log.debug("Ignoring invalid Set-Cookie header (header index #{}) : {}", setCookieIndex, err.toString()); - } - } - } - - cachedCookies = Collections.unmodifiableMap(cookies); - markReadCookies(); - return cachedCookies; - } + public abstract Map> getCachedCookies(); protected final boolean hasReadCookies() { return state == READ; diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/HttpInfos.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/HttpInfos.java index ae88bc76c4..b63136203a 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/HttpInfos.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/HttpInfos.java @@ -15,12 +15,8 @@ */ package reactor.netty5.http; -import java.util.Map; -import java.util.Set; - import io.netty5.handler.codec.http.HttpMethod; import io.netty5.handler.codec.http.HttpVersion; -import io.netty5.handler.codec.http.headers.HttpCookiePair; /** * An Http Reactive Channel with several accessors related to HTTP flow: headers, params, @@ -31,13 +27,6 @@ */ public interface HttpInfos { - /** - * Returns resolved HTTP cookies. - * - * @return Resolved HTTP cookies - */ - Map> cookies(); - /** * Returns the decoded path portion from the {@link #uri()} * diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/client/ClientCookies.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/ClientCookies.java new file mode 100644 index 0000000000..7649e49917 --- /dev/null +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/ClientCookies.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.netty5.http.client; + +import io.netty5.handler.codec.http.headers.HttpHeaders; +import io.netty5.handler.codec.http.headers.HttpSetCookie; +import reactor.netty5.http.Cookies; +import reactor.util.Logger; +import reactor.util.Loggers; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Holder for Set-Cookie headers found in response headers + * + * @author Violeta Georgieva + * @since 2.0.0 + */ +public final class ClientCookies extends Cookies { + static final Logger log = Loggers.getLogger(Cookies.class); + + /** + * Return a new cookies holder from client response headers. + * + * @param headers client response headers + * @return a new cookies holder from client response headers + */ + public static ClientCookies newClientResponseHolder(HttpHeaders headers) { + return new ClientCookies(headers); + } + + final HttpHeaders nettyHeaders; + + Map> cachedCookies; + + ClientCookies(HttpHeaders nettyHeaders) { + this.cachedCookies = Collections.emptyMap(); + this.nettyHeaders = Objects.requireNonNull(nettyHeaders, "nettyHeaders"); + } + + @Override + public Map> getCachedCookies() { + if (!markReadingCookies()) { + for (;;) { + if (hasReadCookies()) { + return cachedCookies; + } + } + } + + Map> cookies = new HashMap<>(); + Iterator setCookieItr = nettyHeaders.getSetCookiesIterator(); + int setCookieIndex = -1; + while (setCookieItr.hasNext()) { + try { + setCookieIndex++; + HttpSetCookie setCookie = setCookieItr.next(); + Set existingCookiesOfName = cookies.computeIfAbsent(setCookie.name(), k -> new HashSet<>()); + existingCookiesOfName.add(setCookie); + } + catch (IllegalArgumentException err) { + // Ignore invalid syntax or whatever decoding error for the current Set-Cookie header. + // Since we don't know which Set-Cookie header is not parsed, we log the Set-Cookie header index number. + if (log.isDebugEnabled()) { + log.debug("Ignoring invalid Set-Cookie header (header index #{}) : {}", setCookieIndex, err.toString()); + } + } + } + + cachedCookies = Collections.unmodifiableMap(cookies); + markReadCookies(); + return cachedCookies; + } +} diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/client/FailedHttpClientRequest.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/FailedHttpClientRequest.java index 2064a4daa4..59e67ca7ad 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/client/FailedHttpClientRequest.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/FailedHttpClientRequest.java @@ -21,7 +21,7 @@ import io.netty5.handler.codec.http.HttpMethod; import io.netty5.handler.codec.http.HttpVersion; import io.netty5.handler.codec.http.headers.HttpCookiePair; -import reactor.netty5.http.Cookies; +import io.netty5.handler.codec.http.headers.HttpSetCookie; import reactor.netty5.http.HttpOperations; import reactor.util.context.ContextView; @@ -67,8 +67,8 @@ public HttpClientRequest addHeader(CharSequence name, CharSequence value) { } @Override - public Map> cookies() { - return Cookies.newClientResponseHolder(headers) + public Map> cookies() { + return ClientCookies.newClientResponseHolder(headers) .getCachedCookies(); } diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientInfos.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientInfos.java index 34df161db5..0423518d73 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientInfos.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientInfos.java @@ -16,10 +16,14 @@ package reactor.netty5.http.client; import io.netty5.handler.codec.http.headers.HttpHeaders; +import io.netty5.handler.codec.http.headers.HttpSetCookie; import reactor.netty5.http.HttpInfos; import reactor.util.annotation.Nullable; import reactor.util.context.ContextView; +import java.util.Map; +import java.util.Set; + /** * An Http Reactive Channel with several accessors related to HTTP flow: resource URL, * information for redirections etc... @@ -28,6 +32,13 @@ */ public interface HttpClientInfos extends HttpInfos { + /** + * Returns resolved HTTP cookies. + * + * @return Resolved HTTP cookies + */ + Map> cookies(); + /** * Return the current {@link ContextView} associated with the Mono/Flux exposed * via {@link HttpClient.ResponseReceiver#response()} or related terminating API. diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientOperations.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientOperations.java index d8620ebef2..14d0d5aff6 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientOperations.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/client/HttpClientOperations.java @@ -54,6 +54,7 @@ import io.netty5.handler.codec.http.HttpUtil; import io.netty5.handler.codec.http.HttpVersion; import io.netty5.handler.codec.http.LastHttpContent; +import io.netty5.handler.codec.http.headers.HttpSetCookie; import io.netty5.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; import io.netty5.handler.timeout.ReadTimeoutHandler; import io.netty5.util.Resource; @@ -69,7 +70,6 @@ import reactor.netty5.NettyPipeline; import reactor.netty5.channel.AbortedException; import reactor.netty5.channel.ChannelOperations; -import reactor.netty5.http.Cookies; import reactor.netty5.http.HttpOperations; import reactor.util.Logger; import reactor.util.Loggers; @@ -217,7 +217,7 @@ public HttpClientOperations withConnection(Consumer withConn } @Override - public Map> cookies() { + public Map> cookies() { ResponseState responseState = this.responseState; if (responseState != null && responseState.cookieHolder != null) { return responseState.cookieHolder.getCachedCookies(); @@ -773,14 +773,14 @@ final void withWebsocketSupport(WebsocketClientSpec websocketClientSpec) { static final class ResponseState { - final HttpResponse response; - final HttpHeaders headers; - final Cookies cookieHolder; + final HttpResponse response; + final HttpHeaders headers; + final ClientCookies cookieHolder; ResponseState(HttpResponse response, HttpHeaders headers) { this.response = response; this.headers = headers; - this.cookieHolder = Cookies.newClientResponseHolder(headers); + this.cookieHolder = ClientCookies.newClientResponseHolder(headers); } } diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/HttpServerInfos.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/HttpServerInfos.java index 85acfa292e..7476d80038 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/HttpServerInfos.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/HttpServerInfos.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; /** * An Http Reactive Channel with several accessors related to HTTP flow: headers, params, @@ -30,6 +31,13 @@ */ public interface HttpServerInfos extends HttpInfos { + /** + * Returns resolved HTTP cookies. + * + * @return Resolved HTTP cookies + */ + Map> cookies(); + /** * Returns resolved HTTP cookies. As opposed to {@link #cookies()}, this * returns all cookies, even if they have the same name. diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/ServerCookies.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/ServerCookies.java index 9862528a19..f3cd7df765 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/ServerCookies.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/ServerCookies.java @@ -25,15 +25,16 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** - * Holder for Cookie headers found from request headers + * Holder for Cookie headers found in request headers * * @author Violeta Georgieva * @since 1.0.8 */ -public final class ServerCookies extends Cookies { +public final class ServerCookies extends Cookies { /** * Return a new cookies holder from server request headers. @@ -45,11 +46,15 @@ public static ServerCookies newServerRequestHolder(HttpHeaders headers) { return new ServerCookies(headers); } + final HttpHeaders nettyHeaders; + Map> allCachedCookies; + Map> cachedCookies; ServerCookies(HttpHeaders nettyHeaders) { - super(nettyHeaders); - allCachedCookies = Collections.emptyMap(); + this.allCachedCookies = Collections.emptyMap(); + this.cachedCookies = Collections.emptyMap(); + this.nettyHeaders = Objects.requireNonNull(nettyHeaders, "nettyHeaders"); } @Override diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH1.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH1.java index d465d217e6..2b91cab5db 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH1.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH1.java @@ -26,7 +26,7 @@ import io.netty5.handler.codec.http.LastHttpContent; import io.netty5.util.concurrent.Future; import reactor.netty5.channel.ChannelOperations; -import reactor.netty5.http.HttpInfos; +import reactor.netty5.http.server.HttpServerInfos; import reactor.util.annotation.Nullable; import java.util.function.Function; @@ -74,7 +74,7 @@ public Future write(ChannelHandlerContext ctx, Object msg) { } ChannelOperations ops = ChannelOperations.get(ctx.channel()); - if (ops instanceof HttpInfos httpInfos) { + if (ops instanceof HttpServerInfos httpInfos) { accessLogArgProvider.cookies(httpInfos.cookies()); } } diff --git a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH2.java b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH2.java index 36f61b5dc3..9f6a6d58eb 100644 --- a/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH2.java +++ b/reactor-netty5-http/src/main/java/reactor/netty5/http/server/logging/AccessLogHandlerH2.java @@ -21,7 +21,7 @@ import io.netty5.handler.codec.http2.Http2HeadersFrame; import io.netty5.util.concurrent.Future; import reactor.netty5.channel.ChannelOperations; -import reactor.netty5.http.HttpInfos; +import reactor.netty5.http.server.HttpServerInfos; import reactor.util.annotation.Nullable; import java.util.function.Function; @@ -62,7 +62,7 @@ public Future write(ChannelHandlerContext ctx, Object msg) { .chunked(true); ChannelOperations ops = ChannelOperations.get(ctx.channel()); - if (ops instanceof HttpInfos httpInfos) { + if (ops instanceof HttpServerInfos httpInfos) { accessLogArgProvider.cookies(httpInfos.cookies()); } } diff --git a/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpCookieHandlingTests.java b/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpCookieHandlingTests.java index f886a6c34b..9d21e55c12 100644 --- a/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpCookieHandlingTests.java +++ b/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpCookieHandlingTests.java @@ -24,6 +24,7 @@ import io.netty5.handler.codec.http.HttpHeaderNames; import io.netty5.handler.codec.http.headers.DefaultHttpSetCookie; import io.netty5.handler.codec.http.headers.HttpCookiePair; +import io.netty5.handler.codec.http.headers.HttpSetCookie; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.netty5.BaseHttpTest; @@ -48,7 +49,7 @@ void clientWillIgnoreMalformedCookies() { .log("server received")))) .bindNow(); - Mono>> cookieResponse = + Mono>> cookieResponse = createClient(disposableServer.port()) .get() .uri("/test") @@ -75,7 +76,7 @@ void clientWithoutCookieGetsANewOneFromServer() { .log("server received")))) .bindNow(); - Mono>> cookieResponse = + Mono>> cookieResponse = createClient(disposableServer.port()) .get() .uri("/test") @@ -86,7 +87,7 @@ void clientWithoutCookieGetsANewOneFromServer() { StepVerifier.create(cookieResponse) .expectNextMatches(l -> { // Suppressed "CollectionUndefinedEquality", the CharSequence is String - Set cookies = l.get("cookie1"); + Set cookies = l.get("cookie1"); return cookies.stream().anyMatch(e -> e.value().toString().equals("test_value")); }) .expectComplete() @@ -103,7 +104,7 @@ void customCookieEncoderDecoder() { .sendString(Mono.just("test"))) .bindNow(); - Mono>> response = + Mono>> response = createClient(disposableServer.port()) //.cookieCodec(ClientCookieEncoder.LAX, ClientCookieDecoder.LAX) .get() @@ -115,7 +116,7 @@ void customCookieEncoderDecoder() { StepVerifier.create(response) .expectNextMatches(map -> { // Suppressed "CollectionUndefinedEquality", the CharSequence is String - Set cookies = map.get("cookie1"); + Set cookies = map.get("cookie1"); return cookies.stream().anyMatch(e -> e.value().toString().equals("test_value")); }) .expectComplete() diff --git a/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpOperationsTest.java b/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpOperationsTest.java index 9410835f11..591c6b1e97 100644 --- a/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpOperationsTest.java +++ b/reactor-netty5-http/src/test/java/reactor/netty5/http/HttpOperationsTest.java @@ -28,16 +28,12 @@ import io.netty5.handler.codec.http.HttpResponseStatus; import io.netty5.handler.codec.http.HttpVersion; import io.netty5.handler.codec.http.LastHttpContent; -import io.netty5.handler.codec.http.headers.HttpCookiePair; import io.netty.contrib.handler.codec.json.JsonObjectDecoder; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.netty5.Connection; -import java.util.Map; -import java.util.Set; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -186,11 +182,6 @@ void testFullPath() { static final class TestHttpInfos implements HttpInfos { String uri; - @Override - public Map> cookies() { - return null; - } - @Override public boolean isKeepAlive() { return false;