Skip to content

Commit

Permalink
Merge branch '4.5.x' into 5.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
sdelamo committed Apr 30, 2024
2 parents fc1473c + 243ef5a commit e8e3211
Show file tree
Hide file tree
Showing 35 changed files with 819 additions and 155 deletions.
2 changes: 1 addition & 1 deletion buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repositories {

dependencies {
implementation "org.aim42:htmlSanityCheck:1.1.6"
implementation "io.micronaut.build.internal:micronaut-gradle-plugins:6.7.0"
implementation "io.micronaut.build.internal:micronaut-gradle-plugins:6.7.1"

implementation "org.tomlj:tomlj:1.1.1"
implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.2"
Expand Down
8 changes: 4 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ logbook-netty = "2.16.0"
log4j = "2.22.1"
micronaut-aws = "4.5.0"
micronaut-groovy = "4.3.0"
micronaut-session = "4.2.0"
micronaut-session = "4.3.0"
micronaut-sql = "5.3.0"
micronaut-test = "4.1.1"
micronaut-validation = "4.4.4"
micronaut-validation = "4.5.0"
micronaut-rxjava2 = "2.3.0"
micronaut-rxjava3 = "3.2.1"
micronaut-rxjava3 = "3.3.0"
micronaut-reactor = "3.3.0"
neo4j-java-driver = "5.17.0"
selenium = "4.9.1"
slf4j = "2.0.13"
smallrye = "6.2.6"
smallrye = "6.3.0"
spock = "2.3-groovy-4.0"
spotbugs = "4.7.1"
systemlambda = "1.2.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public final class HttpVersionSelection {
true
);

private static final HttpVersionSelection WEBSOCKET_1 = new HttpVersionSelection(
HttpVersionSelection.PlaintextMode.HTTP_1,
true,
new String[]{HttpVersionSelection.ALPN_HTTP_1},
false);

private final PlaintextMode plaintextMode;
private final boolean alpn;
private final String[] alpnSupportedProtocols;
Expand Down Expand Up @@ -100,6 +106,17 @@ public static HttpVersionSelection forLegacyVersion(@NonNull HttpVersion httpVer
}
}

/**
* Get the {@link HttpVersionSelection} to be used for a WebSocket connection, which will enable
* ALPN but constrain the mode to HTTP 1.1.
*
* @return The version selection for WebSocket
*/
@NonNull
public static HttpVersionSelection forWebsocket() {
return WEBSOCKET_1;
}

/**
* Construct a version selection from the given client configuration.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public class ConnectionManager {
private final HttpClientConfiguration configuration;
private volatile SslContext sslContext;
private volatile /* QuicSslContext */ Object http3SslContext;
private volatile SslContext websocketSslContext;
private final NettyClientCustomizer clientCustomizer;
private final String informationalServiceId;

Expand All @@ -165,6 +166,7 @@ public class ConnectionManager {
this.configuration = from.configuration;
this.sslContext = from.sslContext;
this.http3SslContext = from.http3SslContext;
this.websocketSslContext = from.websocketSslContext;
this.clientCustomizer = from.clientCustomizer;
this.informationalServiceId = from.informationalServiceId;
this.nettyClientSslBuilder = from.nettyClientSslBuilder;
Expand Down Expand Up @@ -209,6 +211,8 @@ public class ConnectionManager {

final void refresh() {
SslContext oldSslContext = sslContext;
SslContext oldWebsocketSslContext = websocketSslContext;
websocketSslContext = null;
if (configuration.getSslConfiguration().isEnabled()) {
sslContext = nettyClientSslBuilder.build(configuration.getSslConfiguration(), httpVersion);
} else {
Expand All @@ -224,6 +228,7 @@ final void refresh() {
pool.forEachConnection(c -> ((Pool.ConnectionHolder) c).windDownConnection());
}
ReferenceCountUtil.release(oldSslContext);
ReferenceCountUtil.release(oldWebsocketSslContext);
}

/**
Expand Down Expand Up @@ -369,7 +374,9 @@ public final void shutdown() {
}
}
ReferenceCountUtil.release(sslContext);
ReferenceCountUtil.release(websocketSslContext);
sslContext = null;
websocketSslContext = null;
}

/**
Expand Down Expand Up @@ -432,6 +439,32 @@ public final Mono<PoolHandle> connect(DefaultHttpClient.RequestKey requestKey, @
return pools.computeIfAbsent(requestKey, Pool::new).acquire(blockHint);
}

/**
* Builds an {@link SslContext} for the given WebSocket URI if necessary.
*
* @return The {@link SslContext} instance
*/
@Nullable
private SslContext buildWebsocketSslContext(DefaultHttpClient.RequestKey requestKey) {
SslContext sslCtx = websocketSslContext;
if (requestKey.isSecure()) {
if (configuration.getSslConfiguration().isEnabled()) {
if (sslCtx == null) {
synchronized (this) {
sslCtx = websocketSslContext;
if (sslCtx == null) {
sslCtx = nettyClientSslBuilder.build(configuration.getSslConfiguration(), HttpVersionSelection.forWebsocket());
websocketSslContext = sslCtx;
}
}
}
} else if (configuration.getProxyAddress().isEmpty()){
throw decorate(new HttpClientException("Cannot send WSS request. SSL is disabled"));
}
}
return sslCtx;
}

/**
* Connect to a remote websocket. The given {@link ChannelHandler} is added to the pipeline
* when the handshakes complete.
Expand All @@ -448,7 +481,7 @@ final Mono<?> connectForWebsocket(DefaultHttpClient.RequestKey requestKey, Chann
protected void initChannel(@NonNull Channel ch) {
addLogHandler(ch);

SslContext sslContext = buildSslContext(requestKey);
SslContext sslContext = buildWebsocketSslContext(requestKey);
if (sslContext != null) {
ch.pipeline().addLast(configureSslHandler(sslContext.newHandler(ch.alloc(), requestKey.getHost(), requestKey.getPort())));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.micronaut.websocket.exceptions.WebSocketClientException
import jakarta.inject.Inject
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
import spock.lang.Issue
import spock.lang.Specification

import java.util.concurrent.ExecutionException
Expand Down Expand Up @@ -38,6 +39,47 @@ class ClientWebsocketSpec extends Specification {
client.close()
}

@Issue("https://github.com/micronaut-projects/micronaut-core/issues/10744")
void 'websocket bean can connect to echo server over SSL with wss scheme'() {
given:
def ctx = ApplicationContext.run(['spec.name': 'ClientWebsocketSpec'])
def client = ctx.getBean(WebSocketClient)
def registry = ctx.getBean(ClientBeanRegistry)
def mono = Mono.from(client.connect(ClientBean.class, 'wss://websocket-echo.com'))

when:
mono.toFuture().get()

then:
registry.clientBeans.size() == 1
registry.clientBeans[0].opened
!registry.clientBeans[0].autoClosed
!registry.clientBeans[0].onClosed

cleanup:
client.close()
}

void 'websocket bean can connect to echo server over SSL with https scheme'() {
given:
def ctx = ApplicationContext.run(['spec.name': 'ClientWebsocketSpec'])//, "micronaut.http.client.alpn-modes":"http/1.1"])
def client = ctx.getBean(WebSocketClient)
def registry = ctx.getBean(ClientBeanRegistry)
def mono = Mono.from(client.connect(ClientBean.class, 'https://websocket-echo.com'))

when:
mono.toFuture().get()

then:
registry.clientBeans.size() == 1
registry.clientBeans[0].opened
!registry.clientBeans[0].autoClosed
!registry.clientBeans[0].onClosed

cleanup:
client.close()
}

@Singleton
@Requires(property = 'spec.name', value = 'ClientWebsocketSpec')
static class ClientBeanRegistry {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.micronaut.http.netty.cookies

import io.micronaut.http.cookie.Cookie
import io.micronaut.http.cookie.CookieFactory
import spock.lang.Specification

Expand All @@ -9,4 +10,9 @@ class CookieFactorySpec extends Specification {
expect:
CookieFactory.INSTANCE instanceof NettyCookieFactory
}

void "default cookie is a netty cookie with max age undefined"() {
expect:
Cookie.of("SID", "31d4d96e407aad42").getMaxAge() == Cookie.UNDEFINED_MAX_AGE
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.micronaut.http.netty.cookies

import io.micronaut.http.cookie.Cookie
import io.micronaut.http.cookie.HttpCookieFactory
import io.micronaut.http.cookie.SameSite
import io.micronaut.http.cookie.ServerCookieEncoder
import spock.lang.Specification
Expand Down Expand Up @@ -46,6 +47,41 @@ class NettyServerCookieEncoderSpec extends Specification {
expected == result || expected2 == result || expected3 == result
}

void "netty server cookie encoder can correctly encode a cookie from HttpCookieFactory"() {
given:
HttpCookieFactory factory = new HttpCookieFactory();
ServerCookieEncoder cookieEncoder = new NettyServerCookieEncoder()

when:
Cookie cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com")

then:
"SID=31d4d96e407aad42; Path=/; Domain=example.com" == cookieEncoder.encode(cookie)[0]

when:
cookie = factory.create("SID", "31d4d96e407aad42").path("/").domain("example.com").sameSite(SameSite.Strict)

then:
"SID=31d4d96e407aad42; Path=/; Domain=example.com; SameSite=Strict" == cookieEncoder.encode(cookie)[0]

when:
cookie = factory.create("SID", "31d4d96e407aad42").path("/").secure().httpOnly()

then: 'Netty uses HTTPOnly instead of HttpOnly'
"SID=31d4d96e407aad42; Path=/; Secure; HTTPOnly" == cookieEncoder.encode(cookie)[0]

when:
long maxAge = 2592000
cookie = factory.create("id", "a3fWa").maxAge(maxAge)
String result = cookieEncoder.encode(cookie).get(0)
String expected = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge)
String expected2 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge + 1) // To prevent flakiness
String expected3 = "id=a3fWa; Max-Age=2592000; " + Cookie.ATTRIBUTE_EXPIRES + "=" + expires(maxAge - 1) // To prevent flakiness

then:
expected == result || expected2 == result || expected3 == result
}

void "ServerCookieEncoder is NettyServerCookieEncoder"() {
expect:
ServerCookieEncoder.INSTANCE instanceof NettyServerCookieEncoder
Expand Down
51 changes: 51 additions & 0 deletions http/src/main/java/io/micronaut/http/MediaType.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ public class MediaType implements CharSequence {
*/
public static final String EXTENSION_XLS = "xls";

/**
* File extension for GPS Exchange Format files.
*/
public static final String EXTENSION_GPX = "gpx";

/**
* File extension for ZIP archive files.
*/
public static final String EXTENSION_ZIP = "zip";

/**
* File extension for GZIP compressed files.
*/
public static final String EXTENSION_GZIP = "gz";

/**
* Default empty media type array.
*/
Expand Down Expand Up @@ -424,6 +439,36 @@ public class MediaType implements CharSequence {
*/
public static final MediaType IMAGE_WEBP_TYPE = new MediaType(IMAGE_WEBP);

/**
* GPS Exchange Format: application/gpx+xml.
*/
public static final String APPLICATION_GPX_XML = "application/gpx+xml";

/**
* GPS Exchange Format: application/gpx+xml.
*/
public static final MediaType GPX_XML_TYPE = new MediaType(APPLICATION_GPX_XML, EXTENSION_GPX);

/**
* ZIP archive format: application/zip.
*/
public static final String APPLICATION_ZIP = "application/zip";

/**
* ZIP archive format: application/zip.
*/
public static final MediaType ZIP_TYPE = new MediaType(APPLICATION_ZIP);

/**
* GZip compressed data: application/gzip.
*/
public static final String APPLICATION_GZIP = "application/gzip";

/**
* GZip compressed data: application/gzip.
*/
public static final MediaType GZIP_TYPE = new MediaType(APPLICATION_GZIP);

/**
* Parameter {@code "charset"}.
*/
Expand Down Expand Up @@ -645,6 +690,12 @@ public static MediaType of(String mediaType) {
return IMAGE_GIF_TYPE;
case IMAGE_WEBP:
return IMAGE_WEBP_TYPE;
case APPLICATION_GPX_XML:
return GPX_XML_TYPE;
case APPLICATION_GZIP:
return GZIP_TYPE;
case APPLICATION_ZIP:
return ZIP_TYPE;
default:
return new MediaType(mediaType);
}
Expand Down
14 changes: 12 additions & 2 deletions http/src/main/java/io/micronaut/http/cookie/Cookie.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
*/
public interface Cookie extends Comparable<Cookie>, Serializable {

/**
* Constant for undefined MaxAge attribute value.
*/
long UNDEFINED_MAX_AGE = Long.MIN_VALUE;

/**
* @see <a href="https://tools.ietf.org/html/rfc6265#section-4.1.1">The Secure Attribute</a>.
*/
Expand Down Expand Up @@ -66,7 +71,7 @@ public interface Cookie extends Comparable<Cookie>, Serializable {
* @see <a href="https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2">The Max-Age Attribute</a>
*/
String ATTRIBUTE_MAX_AGE = "Max-Age";

/**
* @return The name of the cookie
*/
Expand Down Expand Up @@ -110,6 +115,10 @@ public interface Cookie extends Comparable<Cookie>, Serializable {
boolean isSecure();

/**
* Gets the maximum age of the cookie in seconds. If the max age has not been explicitly set,
* then the value returned will be {@link #UNDEFINED_MAX_AGE}, indicating that the Max-Age
* Attribute should not be written.
*
* @return The maximum age of the cookie in seconds
*/
long getMaxAge();
Expand All @@ -136,7 +145,8 @@ default Optional<SameSite> getSameSite() {
}

/**
* Sets the max age of the cookie in seconds.
* Sets the max age of the cookie in seconds. When not explicitly set, the max age will default
* to {@link #UNDEFINED_MAX_AGE} and cause the Max-Age Attribute not to be encoded.
*
* @param maxAge The max age
* @return This cookie
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class CookieHttpCookieAdapter implements Cookie {

public CookieHttpCookieAdapter(HttpCookie httpCookie) {
this.httpCookie = httpCookie;
if (httpCookie.getMaxAge() == -1) { // HttpCookie.UNDEFINED_MAX_AGE = -1
this.httpCookie.setMaxAge(Cookie.UNDEFINED_MAX_AGE);
}
}

@Override
Expand Down
Loading

0 comments on commit e8e3211

Please sign in to comment.