diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java index c7fd1d255..5d02f3da4 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java @@ -43,11 +43,12 @@ static long SSLContext_new(boolean server, String[] applicationProtocols, BoringSSLCertificateVerifyCallback verifyCallback, BoringSSLTlsextServernameCallback servernameCallback, BoringSSLKeylogCallback keylogCallback, + BoringSSLSessionCallback sessionCallback, int verifyMode, byte[][] subjectNames) { return SSLContext_new0(server, toWireFormat(applicationProtocols), - handshakeCompleteCallback, - certificateCallback, verifyCallback, servernameCallback, keylogCallback, verifyMode, subjectNames); + handshakeCompleteCallback, certificateCallback, verifyCallback, servernameCallback, + keylogCallback, sessionCallback, verifyMode, subjectNames); } private static byte[] toWireFormat(String[] applicationProtocols) { @@ -70,6 +71,7 @@ private static native long SSLContext_new0(boolean server, byte[] applicationProtocols, Object handshakeCompleteCallback, Object certificateCallback, Object verifyCallback, Object servernameCallback, Object keylogCallback, + Object sessionCallback, int verifyDepth, byte[][] subjectNames); static native void SSLContext_set_early_data_enabled(long context, boolean enabled); static native long SSLContext_setSessionCacheSize(long context, long size); diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLHandshakeCompleteCallback.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLHandshakeCompleteCallback.java index aed3d12e8..539add03a 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLHandshakeCompleteCallback.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLHandshakeCompleteCallback.java @@ -26,7 +26,7 @@ final class BoringSSLHandshakeCompleteCallback { @SuppressWarnings("unused") void handshakeComplete(long ssl, byte[] id, String cipher, String protocol, byte[] peerCertificate, byte[][] peerCertificateChain, long creationTime, long timeout, byte[] applicationProtocol) { - QuicheQuicSslEngine engine = map.remove(ssl); + QuicheQuicSslEngine engine = map.get(ssl); if (engine != null) { engine.handshakeFinished(id, cipher, protocol, peerCertificate, peerCertificateChain, creationTime, timeout, applicationProtocol); diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLSessionCallback.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLSessionCallback.java new file mode 100644 index 000000000..94ba56169 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLSessionCallback.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.incubator.codec.quic; + +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +final class BoringSSLSessionCallback { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(BoringSSLSessionCallback.class); + private final QuicClientSessionCache sessionCache; + private final QuicheQuicSslEngineMap engineMap; + + BoringSSLSessionCallback(QuicheQuicSslEngineMap engineMap, QuicClientSessionCache sessionCache) { + this.engineMap = engineMap; + this.sessionCache = sessionCache; + } + + @SuppressWarnings("unused") + void newSession(long ssl, long creationTime, long timeout, byte[] session, boolean isSingleUse, byte[] peerParams) { + if (sessionCache == null) { + return; + } + + QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + logger.warn("engine is null ssl: {}", ssl); + return; + } + + if (peerParams == null) { + peerParams = EmptyArrays.EMPTY_BYTES; + } + if (logger.isDebugEnabled()) { + logger.debug("ssl: {}, session: {}, peerParams: {}", ssl, Arrays.toString(session), + Arrays.toString(peerParams)); + } + byte[] quicSession = toQuicheQuicSession(session, peerParams); + if (quicSession != null) { + logger.debug("save session host={}, port={}", + engine.getSession().getPeerHost(), engine.getSession().getPeerPort()); + sessionCache.saveSession(engine.getSession().getPeerHost(), engine.getSession().getPeerPort(), + TimeUnit.SECONDS.toMillis(creationTime), TimeUnit.SECONDS.toMillis(timeout), + quicSession, isSingleUse); + } + } + + // Mimic the encoding of quiche: https://github.com/cloudflare/quiche/blob/0.10.0/src/lib.rs#L1668 + private static byte[] toQuicheQuicSession(byte[] sslSession, byte[] peerParams) { + if (sslSession != null && peerParams != null) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos)) { + dos.writeLong(sslSession.length); + dos.write(sslSession); + dos.writeLong(peerParams.length); + dos.write(peerParams); + return bos.toByteArray(); + } catch (IOException e) { + return null; + } + } + return null; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/EarlyDataSendCallback.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/EarlyDataSendCallback.java new file mode 100644 index 000000000..7c3e0540a --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/EarlyDataSendCallback.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.incubator.codec.quic; + +/** + * Implementations of this interface can be used to send early data for a {@link QuicChannel}. + */ +@FunctionalInterface +public interface EarlyDataSendCallback { + /** + * Allow to send early-data if possible. Please be aware that early data may be replayable and so may have other + * security concerns then other data. + * + * @param quicChannel the {@link QuicChannel} which will be used to send data on (if any). + */ + void send(QuicChannel quicChannel); +} diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicChannelBootstrap.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicChannelBootstrap.java index da09f61f6..5c7eb199a 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicChannelBootstrap.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicChannelBootstrap.java @@ -50,6 +50,7 @@ public final class QuicChannelBootstrap { private QuicConnectionAddress connectionAddress = QuicConnectionAddress.EPHEMERAL; private ChannelHandler handler; private ChannelHandler streamHandler; + private EarlyDataSendCallback earlyDataSendCallback; /** * Creates a new instance which uses the given {@link Channel} to bootstrap the {@link QuicChannel}. @@ -168,6 +169,17 @@ public QuicChannelBootstrap connectionAddress(QuicConnectionAddress connectionAd return this; } + /** + * Set the {@link EarlyDataSendCallback} to use. + * + * @param earlyDataSendCallback the callback. + * @return this instance. + */ + public QuicChannelBootstrap earlyDataSendCallBack(EarlyDataSendCallback earlyDataSendCallback) { + this.earlyDataSendCallback = ObjectUtil.checkNotNull(earlyDataSendCallback, "earlyDataSendCallback"); + return this; + } + /** * Connects a {@link QuicChannel} to the remote peer and notifies the future once done. * @@ -197,7 +209,8 @@ public Future connect(Promise promise) { } final QuicConnectionAddress address = connectionAddress; QuicChannel channel = QuicheQuicChannel.forClient(parent, (InetSocketAddress) remote, - streamHandler, Quic.toOptionsArray(streamOptions), Quic.toAttributesArray(streamAttrs)); + streamHandler, Quic.toOptionsArray(streamOptions), Quic.toAttributesArray(streamAttrs), + earlyDataSendCallback); Quic.setupChannel(channel, Quic.toOptionsArray(options), Quic.toAttributesArray(attrs), handler, logger); EventLoop eventLoop = parent.eventLoop(); diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicClientSessionCache.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicClientSessionCache.java new file mode 100644 index 000000000..15abc3154 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicClientSessionCache.java @@ -0,0 +1,233 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.incubator.codec.quic; + +import io.netty.util.AsciiString; +import io.netty.util.internal.SystemPropertyUtil; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +final class QuicClientSessionCache { + + private static final int DEFAULT_CACHE_SIZE; + static { + // Respect the same system property as the JDK implementation to make it easy to switch between implementations. + int cacheSize = SystemPropertyUtil.getInt("javax.net.ssl.sessionCacheSize", 20480); + if (cacheSize >= 0) { + DEFAULT_CACHE_SIZE = cacheSize; + } else { + DEFAULT_CACHE_SIZE = 20480; + } + } + + private final AtomicInteger maximumCacheSize = new AtomicInteger(DEFAULT_CACHE_SIZE); + + // Let's use the same default value as OpenSSL does. + // See https://www.openssl.org/docs/man1.1.1/man3/SSL_get_default_timeout.html + private final AtomicInteger sessionTimeout = new AtomicInteger(300); + private int sessionCounter; + + private final Map sessions = + new LinkedHashMap() { + + private static final long serialVersionUID = -7773696788135734448L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + int maxSize = maximumCacheSize.get(); + return maxSize >= 0 && size() > maxSize; + } + }; + + void saveSession(String host, int port, long creationTime, long timeout, byte[] session, boolean isSingleUse) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + synchronized (sessions) { + // Mimic what OpenSSL is doing and expunge every 255 new sessions + // See https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_flush_sessions.html + if (++sessionCounter == 255) { + sessionCounter = 0; + expungeInvalidSessions(); + } + + sessions.put(hostPort, new SessionHolder(creationTime, timeout, session, isSingleUse)); + } + } + } + + byte[] getSession(String host, int port) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + SessionHolder sessionHolder; + synchronized (sessions) { + sessionHolder = sessions.get(hostPort); + if (sessionHolder == null) { + return null; + } + if (sessionHolder.isSingleUse()) { + // Remove session as it should only be re-used once. + sessions.remove(hostPort); + } + } + if (sessionHolder.isValid()) { + return sessionHolder.sessionBytes(); + } + } + return null; + } + + void removeSession(String host, int port) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + synchronized (sessions) { + sessions.remove(hostPort); + } + } + } + + void setSessionTimeout(int seconds) { + int oldTimeout = sessionTimeout.getAndSet(seconds); + if (oldTimeout > seconds) { + // Drain the whole cache as this way we can use the ordering of the LinkedHashMap to detect early + // if there are any other sessions left that are invalid. + clear(); + } + } + + int getSessionTimeout() { + return sessionTimeout.get(); + } + + void setSessionCacheSize(int size) { + long oldSize = maximumCacheSize.getAndSet(size); + if (oldSize > size || size == 0) { + // Just keep it simple for now and drain the whole cache. + clear(); + } + } + + int getSessionCacheSize() { + return maximumCacheSize.get(); + } + + /** + * Clear the cache and free all cached SSL_SESSION*. + */ + void clear() { + synchronized (sessions) { + sessions.clear(); + } + } + + + private void expungeInvalidSessions() { + assert Thread.holdsLock(sessions); + + if (sessions.isEmpty()) { + return; + } + long now = System.currentTimeMillis(); + Iterator> iterator = sessions.entrySet().iterator(); + while (iterator.hasNext()) { + SessionHolder sessionHolder = iterator.next().getValue(); + // As we use a LinkedHashMap we can break the while loop as soon as we find a valid session. + // This is true as we always drain the cache as soon as we change the timeout to a smaller value as + // it was set before. This way its true that the insertion order matches the timeout order. + if (sessionHolder.isValid(now)) { + break; + } + iterator.remove(); + } + } + + private static HostPort keyFor(String host, int port) { + if (host == null && port < 1) { + return null; + } + return new HostPort(host, port); + } + + private static final class SessionHolder { + private final long creationTime; + private final long timeout; + private final byte[] sessionBytes; + private final boolean isSingleUse; + + SessionHolder(long creationTime, long timeout, byte[] session, boolean isSingleUse) { + this.creationTime = creationTime; + this.timeout = timeout; + this.sessionBytes = session; + this.isSingleUse = isSingleUse; + } + + boolean isValid() { + return isValid(System.currentTimeMillis()); + } + + boolean isValid(long current) { + return current <= creationTime + timeout; + } + + boolean isSingleUse() { + return isSingleUse; + } + + byte[] sessionBytes() { + return sessionBytes; + } + } + + /** + * Host / Port tuple used to find a session in the cache. + */ + private static final class HostPort { + private final int hash; + private final String host; + private final int port; + + HostPort(String host, int port) { + this.host = host; + this.port = port; + // Calculate a hashCode that does ignore case. + this.hash = 31 * AsciiString.hashCode(host) + port; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HostPort)) { + return false; + } + HostPort other = (HostPort) obj; + return port == other.port && host.equalsIgnoreCase(other.host); + } + + @Override + public String toString() { + return "HostPort{" + + "host='" + host + '\'' + + ", port=" + port + + '}'; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java index 2aad470ee..a7f7b8d56 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java @@ -351,7 +351,8 @@ public QuicSslContext build() { keyManagerFactory, keyPassword, mapping, earlyData, keylog, applicationProtocols); } else { return new QuicheQuicSslContext(false, sessionCacheSize, sessionTimeout, clientAuth, trustManagerFactory, - keyManagerFactory, keyPassword, mapping, earlyData, keylog, applicationProtocols); + keyManagerFactory, keyPassword, mapping, earlyData, keylog, + applicationProtocols); } } } diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/Quiche.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/Quiche.java index dfe6f1d3c..a32a91507 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/Quiche.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/Quiche.java @@ -330,6 +330,7 @@ static native int quiche_conn_stream_priority( static native byte[] quiche_conn_source_id(long connAddr); static native byte[] quiche_conn_destination_id(long connAddr); + /** * See quiche_conn_stream_recv. */ @@ -468,6 +469,13 @@ static native int quiche_conn_stream_priority( */ static native int quiche_conn_dgram_send(long connAddr, long buf, int size); + /** + * See + * + * quiche_conn_set_session. + */ + static native int quiche_conn_set_session(long connAddr, byte[] sessionBytes); + /** * See * quiche_config_new. diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicChannel.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicChannel.java index ed63714cf..fc8219b0b 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicChannel.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicChannel.java @@ -147,6 +147,7 @@ public void operationComplete(ChannelFuture future) { private volatile QuicheQuicConnection connection; private volatile QuicConnectionAddress remoteIdAddr; private volatile QuicConnectionAddress localIdAdrr; + private final EarlyDataSendCallback earlyDataSendCallback; private static final AtomicLongFieldUpdater UNI_STREAMS_LEFT_UPDATER = AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "uniStreamsLeft"); @@ -159,15 +160,17 @@ public void operationComplete(ChannelFuture future) { private QuicheQuicChannel(Channel parent, boolean server, ByteBuffer key, InetSocketAddress remote, boolean supportsDatagram, ChannelHandler streamHandler, Map.Entry, Object>[] streamOptionsArray, - Map.Entry, Object>[] streamAttrsArray) { - this(parent, server, key, remote, supportsDatagram, streamHandler, streamOptionsArray, streamAttrsArray, null); + Map.Entry, Object>[] streamAttrsArray, + EarlyDataSendCallback earlyDataSendCallback) { + this(parent, server, key, remote, supportsDatagram, streamHandler, streamOptionsArray, streamAttrsArray, null, + earlyDataSendCallback); } private QuicheQuicChannel(Channel parent, boolean server, ByteBuffer key, InetSocketAddress remote, boolean supportsDatagram, ChannelHandler streamHandler, Map.Entry, Object>[] streamOptionsArray, Map.Entry, Object>[] streamAttrsArray, - Consumer timeoutTask) { + Consumer timeoutTask, EarlyDataSendCallback earlyDataSendCallback) { super(parent); config = new QuicheQuicChannelConfig(this); this.server = server; @@ -181,14 +184,16 @@ private QuicheQuicChannel(Channel parent, boolean server, ByteBuffer key, this.streamHandler = streamHandler; this.streamOptionsArray = streamOptionsArray; this.streamAttrsArray = streamAttrsArray; + this.earlyDataSendCallback = earlyDataSendCallback; timeoutHandler = new TimeoutHandler(timeoutTask); } static QuicheQuicChannel forClient(Channel parent, InetSocketAddress remote, ChannelHandler streamHandler, Map.Entry, Object>[] streamOptionsArray, - Map.Entry, Object>[] streamAttrsArray) { + Map.Entry, Object>[] streamAttrsArray, + EarlyDataSendCallback earlyDataSendCallback) { return new QuicheQuicChannel(parent, false, null, remote, false, streamHandler, - streamOptionsArray, streamAttrsArray); + streamOptionsArray, streamAttrsArray, earlyDataSendCallback); } static QuicheQuicChannel forServer(Channel parent, ByteBuffer key, InetSocketAddress remote, @@ -196,7 +201,7 @@ static QuicheQuicChannel forServer(Channel parent, ByteBuffer key, InetSocketAdd Map.Entry, Object>[] streamOptionsArray, Map.Entry, Object>[] streamAttrsArray) { return new QuicheQuicChannel(parent, true, key, remote, supportsDatagram, - streamHandler, streamOptionsArray, streamAttrsArray); + streamHandler, streamOptionsArray, streamAttrsArray, null); } static QuicheQuicChannel forServer(Channel parent, ByteBuffer key, InetSocketAddress remote, @@ -205,7 +210,7 @@ static QuicheQuicChannel forServer(Channel parent, ByteBuffer key, InetSocketAdd Map.Entry, Object>[] streamAttrsArray, Consumer timeoutTask) { return new QuicheQuicChannel(parent, true, key, remote, supportsDatagram, - streamHandler, streamOptionsArray, streamAttrsArray, timeoutTask); + streamHandler, streamOptionsArray, streamAttrsArray, timeoutTask, null); } @Override @@ -306,6 +311,14 @@ private void connect(Function engineProvid return; } attachQuicheConnection(connection); + QuicClientSessionCache sessionCache = quicheEngine.ctx.getSessionCache(); + if (sessionCache != null) { + byte[] sessionBytes = sessionCache + .getSession(quicheEngine.getSession().getPeerHost(), quicheEngine.getSession().getPeerPort()); + if (sessionBytes != null) { + Quiche.quiche_conn_set_session(connection.address(), sessionBytes); + } + } this.supportsDatagram = supportsDatagram; key = connectId; } finally { @@ -1463,6 +1476,9 @@ private QuicheQuicStreamChannel addNewStreamChannel(long streamId) { void finishConnect() { assert !server; if (connectionSend()) { + if (earlyDataSendCallback != null) { + earlyDataSendCallback.send(this); + } flushParent(); } } diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicConnection.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicConnection.java index db6820302..31a097ff5 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicConnection.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicConnection.java @@ -82,6 +82,7 @@ void free() { if (connection != -1) { try { Quiche.quiche_conn_free(connection); + engine.ctx.remove(engine); release = true; refCnt.release(); } finally { diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java index 9ddd37427..793e3e2a8 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java @@ -62,6 +62,7 @@ final class QuicheQuicSslContext extends QuicSslContext { private long sessionTimeout; private final QuicheQuicSslSessionContext sessionCtx; private final QuicheQuicSslEngineMap engineMap = new QuicheQuicSslEngineMap(); + private final QuicClientSessionCache sessionCache; final NativeSslContext nativeSslContext; QuicheQuicSslContext(boolean server, long sessionTimeout, long sessionCacheSize, @@ -95,17 +96,28 @@ final class QuicheQuicSslContext extends QuicSslContext { } else { keyManager = chooseKeyManager(keyManagerFactory); } + sessionCache = server ? null : new QuicClientSessionCache(); int verifyMode = server ? boringSSLVerifyModeForServer(this.clientAuth) : BoringSSL.SSL_VERIFY_PEER; nativeSslContext = new NativeSslContext(BoringSSL.SSLContext_new(server, applicationProtocols, new BoringSSLHandshakeCompleteCallback(engineMap), new BoringSSLCertificateCallback(engineMap, keyManager, password), new BoringSSLCertificateVerifyCallback(engineMap, trustManager), mapping == null ? null : new BoringSSLTlsextServernameCallback(engineMap, mapping), - keylog ? new BoringSSLKeylogCallback() : null, verifyMode, + keylog ? new BoringSSLKeylogCallback() : null, + server ? null : new BoringSSLSessionCallback(engineMap, sessionCache), verifyMode, BoringSSL.subjectNames(trustManager.getAcceptedIssuers()))); apn = new QuicheQuicApplicationProtocolNegotiator(applicationProtocols); - this.sessionCacheSize = BoringSSL.SSLContext_setSessionCacheSize(nativeSslContext.address(), sessionCacheSize); - this.sessionTimeout = BoringSSL.SSLContext_setSessionCacheTimeout(nativeSslContext.address(), sessionTimeout); + if (this.sessionCache != null) { + // Cache is handled via our own implementation. + this.sessionCache.setSessionCacheSize((int) sessionCacheSize); + this.sessionCache.setSessionTimeout((int) sessionTimeout); + } else { + // Cache is handled by BoringSSL internally + this.sessionCacheSize = BoringSSL.SSLContext_setSessionCacheSize( + nativeSslContext.address(), sessionCacheSize); + this.sessionTimeout = BoringSSL.SSLContext_setSessionCacheTimeout( + nativeSslContext.address(), sessionTimeout); + } if (earlyData != null) { BoringSSL.SSLContext_set_early_data_enabled(nativeSslContext.address(), earlyData); } @@ -196,7 +208,12 @@ long add(QuicheQuicSslEngine engine) { */ void remove(QuicheQuicSslEngine engine) { QuicheQuicSslEngine removed = engineMap.remove(engine.connection.ssl); - assert removed == engine; + assert removed == null || removed == engine; + engine.removeSessionFromCacheIfInvalid(); + } + + QuicClientSessionCache getSessionCache() { + return sessionCache; } @Override @@ -210,13 +227,25 @@ public List cipherSuites() { } @Override - public synchronized long sessionCacheSize() { - return sessionCacheSize; + public long sessionCacheSize() { + if (sessionCache != null) { + return sessionCache.getSessionCacheSize(); + } else { + synchronized (this) { + return sessionCacheSize; + } + } } @Override - public synchronized long sessionTimeout() { - return sessionTimeout; + public long sessionTimeout() { + if (sessionCache != null) { + return sessionCache.getSessionTimeout(); + } else { + synchronized (this) { + return sessionTimeout; + } + } } @Override @@ -281,11 +310,19 @@ protected void finalize() throws Throwable { } void setSessionTimeout(int seconds) throws IllegalArgumentException { - sessionTimeout = BoringSSL.SSLContext_setSessionCacheTimeout(nativeSslContext.address(), seconds); + if (sessionCache != null) { + sessionCache.setSessionTimeout(seconds); + } else { + sessionTimeout = BoringSSL.SSLContext_setSessionCacheTimeout(nativeSslContext.address(), seconds); + } } void setSessionCacheSize(int size) throws IllegalArgumentException { - sessionCacheSize = BoringSSL.SSLContext_setSessionCacheSize(nativeSslContext.address(), size); + if (sessionCache != null) { + sessionCache.setSessionCacheSize(size); + } else { + sessionCacheSize = BoringSSL.SSLContext_setSessionCacheSize(nativeSslContext.address(), size); + } } @SuppressWarnings("deprecation") diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslEngine.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslEngine.java index 87237a5fa..9983cbb7e 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslEngine.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslEngine.java @@ -263,14 +263,20 @@ synchronized void handshakeFinished(byte[] id, String cipher, String protocol, b handshakeFinished = true; } + void removeSessionFromCacheIfInvalid() { + session.removeFromCacheIfInvalid(); + } + private final class QuicheQuicSslSession implements SSLSession { private X509Certificate[] x509PeerCerts; private Certificate[] peerCerts; private String protocol; private String cipher; private byte[] id; - private long creationTime; - private long timeout; + private long creationTime = -1; + private long timeout = -1; + private boolean invalid; + private long lastAccessedTime = -1; // lazy init for memory reasons private Map values; @@ -291,6 +297,22 @@ void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCer this.protocol = protocol; this.creationTime = creationTime * 1000L; this.timeout = timeout * 1000L; + lastAccessedTime = System.currentTimeMillis(); + } + } + + void removeFromCacheIfInvalid() { + if (!isValid()) { + // Shouldn't be re-used again + removeFromCache(); + } + } + + private void removeFromCache() { + // Shouldn't be re-used again + QuicClientSessionCache cache = ctx.getSessionCache(); + if (cache != null) { + cache.removeSession(getPeerHost(), getPeerPort()); } } @@ -365,18 +387,25 @@ public long getCreationTime() { @Override public long getLastAccessedTime() { - return getCreationTime(); + return lastAccessedTime; } @Override public void invalidate() { - // NOOP + boolean removeFromCache; + synchronized (this) { + removeFromCache = !invalid; + invalid = true; + } + if (removeFromCache) { + removeFromCache(); + } } @Override public boolean isValid() { synchronized (QuicheQuicSslEngine.this) { - return System.currentTimeMillis() - timeout < creationTime; + return !invalid && System.currentTimeMillis() - timeout < creationTime; } } diff --git a/codec-native-quic/src/main/c/netty_quic_boringssl.c b/codec-native-quic/src/main/c/netty_quic_boringssl.c index 3cbfe44e8..a958bebed 100644 --- a/codec-native-quic/src/main/c/netty_quic_boringssl.c +++ b/codec-native-quic/src/main/c/netty_quic_boringssl.c @@ -49,6 +49,9 @@ static jmethodID servernameCallbackMethod = NULL; static jclass keylogCallbackClass = NULL; static jmethodID keylogCallbackMethod = NULL; +static jclass sessionCallbackClass = NULL; +static jmethodID sessionCallbackMethod = NULL; + static jclass byteArrayClass = NULL; static jclass stringClass = NULL; @@ -57,6 +60,7 @@ static int verifyCallbackIdx = -1; static int certificateCallbackIdx = -1; static int servernameCallbackIdx = -1; static int keylogCallbackIdx = -1; +static int sessionCallbackIdx = -1; static int alpn_data_idx = -1; static int crypto_buffer_pool_idx = -1; @@ -154,6 +158,17 @@ static jobjectArray stackToArray(JNIEnv *e, const STACK_OF(CRYPTO_BUFFER)* stack return array; } +static jbyteArray to_byte_array(JNIEnv* env, uint8_t* bytes, size_t len) { + if (bytes == NULL || len == 0) { + return NULL; + } + jbyteArray array = (*env)->NewByteArray(env, len); + if (array == NULL) { + return NULL; + } + (*env)->SetByteArrayRegion(env,array, 0, len, (jbyte*) bytes); + return array; +} enum ssl_verify_result_t quic_SSL_cert_custom_verify(SSL* ssl, uint8_t *out_alert) { enum ssl_verify_result_t ret = ssl_verify_invalid; @@ -597,12 +612,65 @@ void keylog_callback(const SSL* ssl, const char* line) { (*e)->CallVoidMethod(e, keylogCallback, keylogCallbackMethod, (jlong) ssl, keyString); } -static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean server, jbyteArray alpn_protos, jobject handshakeCompleteCallback, jobject certificateCallback, jobject verifyCallback, jobject servernameCallback, jobject keylogCallback, jint verifyMode, jobjectArray subjectNames) { +// Always return 0 as we serialize the session / params to byte[] and so no take ownership. +// See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_sess_set_new_cb +int new_session_callback(SSL *ssl, SSL_SESSION *session) { + SSL_CTX* ctx = SSL_get_SSL_CTX(ssl); + if (ctx == NULL) { + return 0; + } + + JNIEnv* e = NULL; + if (quic_get_java_env(&e) != JNI_OK) { + return 0; + } + + jobject sessionCallback = SSL_CTX_get_ex_data(ctx, sessionCallbackIdx); + if (sessionCallback == NULL) { + return 0; + } + + uint8_t *session_data = NULL; + size_t session_data_len = 0; + if (SSL_SESSION_to_bytes(session, &session_data, &session_data_len) == 0) { + // Get session error + return 0; + } + + jbyteArray sessionBytes = to_byte_array(e, session_data, session_data_len); + // We need to explicit free the session_data after we copied it to byte[]. + // See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_SESSION_to_bytes + OPENSSL_free((void *)session_data); + if (sessionBytes == NULL) { + // Get session error + return 0; + } + + jbyteArray peerParamsBytes = NULL; + // There is not need to explicit free peer_params as it will be freed as soon as SSL*. + // See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_get_peer_quic_transport_params + const uint8_t *peer_params = NULL; + size_t peer_params_len = 0; + SSL_get_peer_quic_transport_params((SSL*) ssl, &peer_params, &peer_params_len); + if (peer_params_len != 0) { + peerParamsBytes = to_byte_array(e, (uint8_t *) peer_params, peer_params_len); + } + + jboolean singleUse = SSL_SESSION_should_be_single_use(session) == 1 ? JNI_TRUE : JNI_FALSE; + + // Execute the java callback + (*e)->CallVoidMethod(e, sessionCallback, sessionCallbackMethod, (jlong) ssl, (jlong) SSL_SESSION_get_time(session), (jlong) SSL_SESSION_get_timeout(session), sessionBytes, singleUse, peerParamsBytes); + + return 0; +} + +static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean server, jbyteArray alpn_protos, jobject handshakeCompleteCallback, jobject certificateCallback, jobject verifyCallback, jobject servernameCallback, jobject keylogCallback, jobject sessionCallback, jint verifyMode, jobjectArray subjectNames) { jobject handshakeCompleteCallbackRef = NULL; jobject certificateCallbackRef = NULL; jobject verifyCallbackRef = NULL; jobject servernameCallbackRef = NULL; jobject keylogCallbackRef = NULL; + jobject sessionCallbackRef = NULL; if ((handshakeCompleteCallbackRef = (*env)->NewGlobalRef(env, handshakeCompleteCallback)) == NULL) { goto error; @@ -628,6 +696,12 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean } } + if (sessionCallback != NULL) { + if ((sessionCallbackRef = (*env)->NewGlobalRef(env, sessionCallback)) == NULL) { + goto error; + } + } + SSL_CTX *ctx = SSL_CTX_new(TLS_with_buffers_method()); // When using BoringSSL we want to use CRYPTO_BUFFER to reduce memory usage and minimize overhead as we do not need // X509* at all and just need the raw bytes of the certificates to construct our Java X509Certificate. @@ -659,6 +733,14 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean SSL_CTX_set_ex_data(ctx, keylogCallbackIdx, keylogCallbackRef); SSL_CTX_set_keylog_callback(ctx, keylog_callback); } + + if (sessionCallbackRef != NULL) { + SSL_CTX_set_ex_data(ctx, sessionCallbackIdx, sessionCallbackRef); + // The internal cache is never used on a client, this only enables the callbacks. + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT); + SSL_CTX_sess_set_new_cb(ctx, new_session_callback); + } + // Use a pool for our certificates so we can share these across connections. SSL_CTX_set_ex_data(ctx, crypto_buffer_pool_idx, CRYPTO_BUFFER_POOL_new()); @@ -724,6 +806,11 @@ static void netty_boringssl_SSLContext_free(JNIEnv* env, jclass clazz, jlong ctx (*env)->DeleteGlobalRef(env, keylogCallbackRef); } + jobject sessionCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, sessionCallbackIdx); + if (sessionCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, sessionCallbackRef); + } + alpn_data* data = SSL_CTX_get_ex_data(ssl_ctx, alpn_data_idx); OPENSSL_free(data); @@ -744,8 +831,9 @@ static jlong netty_boringssl_SSLContext_setSessionCacheTimeout(JNIEnv* env, jcla static jlong netty_boringssl_SSLContext_setSessionCacheSize(JNIEnv* env, jclass clazz, jlong ctx, jlong size) { if (size >= 0) { SSL_CTX* ssl_ctx = (SSL_CTX*) ctx; - // Caching only works on the server side for now. - SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER); + int mode = SSL_CTX_get_session_cache_mode(ssl_ctx); + // Internal Cache only works on the server side for now. + SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER | mode); return SSL_CTX_sess_set_cache_size(ssl_ctx, size); } @@ -864,7 +952,7 @@ static const JNINativeMethod statically_referenced_fixed_method_table[] = { static const jint statically_referenced_fixed_method_table_size = sizeof(statically_referenced_fixed_method_table) / sizeof(statically_referenced_fixed_method_table[0]); static const JNINativeMethod fixed_method_table[] = { - { "SSLContext_new0", "(Z[BLjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I[[B)J", (void *) netty_boringssl_SSLContext_new0 }, + { "SSLContext_new0", "(Z[BLjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I[[B)J", (void *) netty_boringssl_SSLContext_new0 }, { "SSLContext_free", "(J)V", (void *) netty_boringssl_SSLContext_free }, { "SSLContext_setSessionCacheTimeout", "(JJ)J", (void *) netty_boringssl_SSLContext_setSessionCacheTimeout }, { "SSLContext_setSessionCacheSize", "(JJ)J", (void *) netty_boringssl_SSLContext_setSessionCacheSize }, @@ -934,11 +1022,16 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { NETTY_JNI_UTIL_LOAD_CLASS(env, keylogCallbackClass, name, done); NETTY_JNI_UTIL_GET_METHOD(env, keylogCallbackClass, keylogCallbackMethod, "logKey", "(JLjava/lang/String;)V", done); + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/incubator/codec/quic/BoringSSLSessionCallback", name, done); + NETTY_JNI_UTIL_LOAD_CLASS(env, sessionCallbackClass, name, done); + NETTY_JNI_UTIL_GET_METHOD(env, sessionCallbackClass, sessionCallbackMethod, "newSession", "(JJJ[BZ[B)V", done); + verifyCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); certificateCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); handshakeCompleteCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); servernameCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); keylogCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + sessionCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); alpn_data_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); crypto_buffer_pool_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); @@ -959,6 +1052,7 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { NETTY_JNI_UTIL_UNLOAD_CLASS(env, handshakeCompleteCallbackClass); NETTY_JNI_UTIL_UNLOAD_CLASS(env, servernameCallbackClass); NETTY_JNI_UTIL_UNLOAD_CLASS(env, keylogCallbackClass); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, sessionCallbackClass); } return ret; } @@ -970,6 +1064,7 @@ void netty_boringssl_JNI_OnUnload(JNIEnv* env, const char* packagePrefix) { NETTY_JNI_UTIL_UNLOAD_CLASS(env, handshakeCompleteCallbackClass); NETTY_JNI_UTIL_UNLOAD_CLASS(env, servernameCallbackClass); NETTY_JNI_UTIL_UNLOAD_CLASS(env, keylogCallbackClass); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, sessionCallbackClass); netty_jni_util_unregister_natives(env, packagePrefix, STATICALLY_CLASSNAME); netty_jni_util_unregister_natives(env, packagePrefix, CLASSNAME); diff --git a/codec-native-quic/src/main/c/netty_quic_quiche.c b/codec-native-quic/src/main/c/netty_quic_quiche.c index 210a102ae..09da7d25a 100644 --- a/codec-native-quic/src/main/c/netty_quic_quiche.c +++ b/codec-native-quic/src/main/c/netty_quic_quiche.c @@ -482,6 +482,12 @@ static jint netty_quiche_conn_dgram_send(JNIEnv* env, jclass clazz, jlong conn, return (jint) quiche_conn_dgram_send((quiche_conn *) conn, (uint8_t *) buf, (size_t) buf_len); } +static jint netty_quiche_conn_set_session(JNIEnv* env, jclass clazz, jlong conn, jbyteArray sessionBytes) { + int buf_len = (*env)->GetArrayLength(env, sessionBytes); + uint8_t* buf = (uint8_t*) (*env)->GetByteArrayElements(env, sessionBytes, 0); + return (jint) quiche_conn_set_session((quiche_conn *) conn, (uint8_t *) buf, (size_t) buf_len); +} + static jlong netty_quiche_config_new(JNIEnv* env, jclass clazz, jint version) { quiche_config* config = quiche_config_new((uint32_t) version); return config == NULL ? -1 : (jlong) config; @@ -713,6 +719,7 @@ static const JNINativeMethod fixed_method_table[] = { { "quiche_conn_dgram_recv_front_len", "(J)I", (void* ) netty_quiche_conn_dgram_recv_front_len }, { "quiche_conn_dgram_recv", "(JJI)I", (void* ) netty_quiche_conn_dgram_recv }, { "quiche_conn_dgram_send", "(JJI)I", (void* ) netty_quiche_conn_dgram_send }, + { "quiche_conn_set_session", "(J[B)I", (void* ) netty_quiche_conn_set_session }, { "quiche_config_new", "(I)J", (void *) netty_quiche_config_new }, { "quiche_config_enable_dgram", "(JZII)V", (void *) netty_quiche_config_enable_dgram }, { "quiche_config_grease", "(JZ)V", (void *) netty_quiche_config_grease }, diff --git a/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/example/QuicClientZeroRTTExample.java b/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/example/QuicClientZeroRTTExample.java new file mode 100644 index 000000000..05d55ef22 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/example/QuicClientZeroRTTExample.java @@ -0,0 +1,139 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.incubator.codec.quic.example; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.incubator.codec.quic.EarlyDataSendCallback; +import io.netty.incubator.codec.quic.QuicChannel; +import io.netty.incubator.codec.quic.QuicChannelBootstrap; +import io.netty.incubator.codec.quic.QuicClientCodecBuilder; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import io.netty.incubator.codec.quic.QuicStreamType; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; +import io.netty.util.concurrent.Future; + +public final class QuicClientZeroRTTExample { + + private QuicClientZeroRTTExample() { } + + public static void main(String[] args) throws Exception { + QuicSslContext context = QuicSslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE). + applicationProtocols("http/0.9").earlyData(true).build(); + + newChannelAndSendData(context, null); + newChannelAndSendData(context, new EarlyDataSendCallback() { + @Override + public void send(QuicChannel quicChannel) { + createStream(quicChannel).addListener(f -> { + if (f.isSuccess()) { + QuicStreamChannel streamChannel = (QuicStreamChannel) f.getNow(); + streamChannel.writeAndFlush( + Unpooled.copiedBuffer("0rtt stream data\r\n", CharsetUtil.US_ASCII)); + } + }); + } + }); + } + + static void newChannelAndSendData(QuicSslContext context, EarlyDataSendCallback earlyDataSendCallback) throws Exception { + NioEventLoopGroup group = new NioEventLoopGroup(1); + try { + ChannelHandler codec = new QuicClientCodecBuilder() + .sslEngineProvider(q -> context.newEngine(q.alloc(), "localhost", 9999)) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + // As we don't want to support remote initiated streams just setup the limit for local initiated + // streams in this example. + .initialMaxStreamDataBidirectionalLocal(1000000) + .build(); + + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(0).sync().channel(); + + + QuicChannelBootstrap quicChannelBootstrap = QuicChannel.newBootstrap(channel) + .streamHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // As we did not allow any remote initiated streams we will never see this method called. + // That said just let us keep it here to demonstrate that this handle would be called + // for each remote initiated stream. + ctx.close(); + } + }) + .remoteAddress(new InetSocketAddress(NetUtil.LOCALHOST4, 9999)); + + if (earlyDataSendCallback != null) { + quicChannelBootstrap.earlyDataSendCallBack(earlyDataSendCallback); + } + + QuicChannel quicChannel = quicChannelBootstrap + .connect() + .get(); + + QuicStreamChannel streamChannel = createStream(quicChannel).sync().getNow(); + // Write the data and send the FIN. After this its not possible anymore to write any more data. + streamChannel.writeAndFlush(Unpooled.copiedBuffer("Bye\r\n", CharsetUtil.US_ASCII)) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + streamChannel.closeFuture().sync(); + quicChannel.closeFuture().sync(); + channel.close().sync(); + } finally { + group.shutdownGracefully(); + } + } + + static Future createStream(QuicChannel quicChannel) { + return quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf byteBuf = (ByteBuf) msg; + System.err.println(byteBuf.toString(CharsetUtil.US_ASCII)); + byteBuf.release(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + // Close the connection once the remote peer did send the FIN for this stream. + ((QuicChannel) ctx.channel().parent()).close(true, 0, + ctx.alloc().directBuffer(16) + .writeBytes(new byte[]{'k', 't', 'h', 'x', 'b', 'y', 'e'})); + } + } + }); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/example/QuicServerZeroRTTExample.java b/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/example/QuicServerZeroRTTExample.java new file mode 100644 index 000000000..f1afe8b9d --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/example/QuicServerZeroRTTExample.java @@ -0,0 +1,121 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.incubator.codec.quic.example; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.quic.NoValidationQuicTokenHandler; +import io.netty.incubator.codec.quic.QuicChannel; +import io.netty.incubator.codec.quic.QuicServerCodecBuilder; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +public final class QuicServerZeroRTTExample { + + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicServerZeroRTTExample.class); + + private QuicServerZeroRTTExample() { } + + public static void main(String[] args) throws Exception { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + QuicSslContext context = QuicSslContextBuilder.forServer( + selfSignedCertificate.privateKey(), null, selfSignedCertificate.certificate()) + .applicationProtocols("http/0.9").earlyData(true).build(); + NioEventLoopGroup group = new NioEventLoopGroup(1); + ChannelHandler codec = new QuicServerCodecBuilder().sslContext(context) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + // Configure some limits for the maximal number of streams (and the data) that we want to handle. + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .initialMaxStreamsUnidirectional(100) + + // Setup a token handler. In a production system you would want to implement and provide your custom + // one. + .tokenHandler(NoValidationQuicTokenHandler.INSTANCE) + // ChannelHandler that is added into QuicChannel pipeline. + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + // Create streams etc.. + } + + public void channelInactive(ChannelHandlerContext ctx) { + ((QuicChannel) ctx.channel()).collectStats().addListener(f -> { + if (f.isSuccess()) { + LOGGER.info("Connection closed: {}", f.getNow()); + } + }); + } + + @Override + public boolean isSharable() { + return true; + } + }) + .streamHandler(new ChannelInitializer() { + @Override + protected void initChannel(QuicStreamChannel ch) { + // Add a LineBasedFrameDecoder here as we just want to do some simple HTTP 0.9 handling. + ch.pipeline().addLast(new LineBasedFrameDecoder(1024)) + .addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf byteBuf = (ByteBuf) msg; + try { + if (byteBuf.toString(CharsetUtil.US_ASCII).trim().equals("Bye")) { + ByteBuf buffer = ctx.alloc().directBuffer(); + buffer.writeCharSequence("Bye\r\n", CharsetUtil.US_ASCII); + // Write the buffer and shutdown the output by writing a FIN. + ctx.writeAndFlush(buffer).addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + } finally { + byteBuf.release(); + } + } + }); + } + }).build(); + try { + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(new InetSocketAddress(9999)).sync().channel(); + channel.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/pom.xml b/pom.xml index 93a108d09..34d392101 100644 --- a/pom.xml +++ b/pom.xml @@ -265,7 +265,7 @@ false ${test.argLine} - + org.apache.felix