diff --git a/netty-channel-epoll/build.gradle b/netty-channel-epoll/build.gradle new file mode 100644 index 0000000..1f0cb7a --- /dev/null +++ b/netty-channel-epoll/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':netty-channel') + api project(':netty-channel-unix') +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java new file mode 100644 index 0000000..915e265 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java @@ -0,0 +1,796 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.AbstractChannel; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.EventLoop; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.SocketChannelConfig; +import io.netty.channel.unix.FileDescriptor; +import io.netty.channel.unix.IovArray; +import io.netty.channel.unix.Socket; +import io.netty.channel.unix.UnixChannel; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AlreadyConnectedException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ConnectionPendingException; +import java.nio.channels.NotYetConnectedException; +import java.nio.channels.UnresolvedAddressException; +import java.util.concurrent.TimeUnit; + +import static io.netty.channel.internal.ChannelUtils.WRITE_STATUS_SNDBUF_FULL; +import static io.netty.channel.unix.UnixChannelUtil.computeRemoteAddr; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +abstract class AbstractEpollChannel extends AbstractChannel implements UnixChannel { + private static final ChannelMetadata METADATA = new ChannelMetadata(false); + protected final LinuxSocket socket; + /** + * The future of the current connection attempt. If not null, subsequent + * connection attempts will fail. + */ + private ChannelPromise connectPromise; + private Future connectTimeoutFuture; + private SocketAddress requestedRemoteAddress; + + private volatile SocketAddress local; + private volatile SocketAddress remote; + + protected int flags = Native.EPOLLET; + boolean inputClosedSeenErrorOnRead; + boolean epollInReadyRunnablePending; + + protected volatile boolean active; + + AbstractEpollChannel(LinuxSocket fd) { + this(null, fd, false); + } + + AbstractEpollChannel(Channel parent, LinuxSocket fd, boolean active) { + super(parent); + this.socket = checkNotNull(fd, "fd"); + this.active = active; + if (active) { + // Directly cache the remote and local addresses + // See https://github.com/netty/netty/issues/2359 + this.local = fd.localAddress(); + this.remote = fd.remoteAddress(); + } + } + + AbstractEpollChannel(Channel parent, LinuxSocket fd, SocketAddress remote) { + super(parent); + this.socket = checkNotNull(fd, "fd"); + this.active = true; + // Directly cache the remote and local addresses + // See https://github.com/netty/netty/issues/2359 + this.remote = remote; + this.local = fd.localAddress(); + } + + static boolean isSoErrorZero(Socket fd) { + try { + return fd.getSoError() == 0; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + protected void setFlag(int flag) throws IOException { + if (!isFlagSet(flag)) { + flags |= flag; + modifyEvents(); + } + } + + void clearFlag(int flag) throws IOException { + if (isFlagSet(flag)) { + flags &= ~flag; + modifyEvents(); + } + } + + boolean isFlagSet(int flag) { + return (flags & flag) != 0; + } + + @Override + public final FileDescriptor fd() { + return socket; + } + + @Override + public abstract EpollChannelConfig config(); + + @Override + public boolean isActive() { + return active; + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + protected void doClose() throws Exception { + active = false; + // Even if we allow half closed sockets we should give up on reading. Otherwise we may allow a read attempt on a + // socket which has not even been connected yet. This has been observed to block during unit tests. + inputClosedSeenErrorOnRead = true; + try { + ChannelPromise promise = connectPromise; + if (promise != null) { + // Use tryFailure() instead of setFailure() to avoid the race against cancel(). + promise.tryFailure(new ClosedChannelException()); + connectPromise = null; + } + + Future future = connectTimeoutFuture; + if (future != null) { + future.cancel(false); + connectTimeoutFuture = null; + } + + if (isRegistered()) { + // Need to check if we are on the EventLoop as doClose() may be triggered by the GlobalEventExecutor + // if SO_LINGER is used. + // + // See https://github.com/netty/netty/issues/7159 + EventLoop loop = eventLoop(); + if (loop.inEventLoop()) { + doDeregister(); + } else { + loop.execute(new Runnable() { + @Override + public void run() { + try { + doDeregister(); + } catch (Throwable cause) { + pipeline().fireExceptionCaught(cause); + } + } + }); + } + } + } finally { + socket.close(); + } + } + + void resetCachedAddresses() { + local = socket.localAddress(); + remote = socket.remoteAddress(); + } + + @Override + protected void doDisconnect() throws Exception { + doClose(); + } + + @Override + protected boolean isCompatible(EventLoop loop) { + return loop instanceof EpollEventLoop; + } + + @Override + public boolean isOpen() { + return socket.isOpen(); + } + + @Override + protected void doDeregister() throws Exception { + ((EpollEventLoop) eventLoop()).remove(this); + } + + @Override + protected final void doBeginRead() throws Exception { + // Channel.read() or ChannelHandlerContext.read() was called + final AbstractEpollUnsafe unsafe = (AbstractEpollUnsafe) unsafe(); + unsafe.readPending = true; + + // We must set the read flag here as it is possible the user didn't read in the last read loop, the + // executeEpollInReadyRunnable could read nothing, and if the user doesn't explicitly call read they will + // never get data after this. + setFlag(Native.EPOLLIN); + + // If EPOLL ET mode is enabled and auto read was toggled off on the last read loop then we may not be notified + // again if we didn't consume all the data. So we force a read operation here if there maybe more data. + if (unsafe.maybeMoreDataToRead) { + unsafe.executeEpollInReadyRunnable(config()); + } + } + + final boolean shouldBreakEpollInReady(ChannelConfig config) { + return socket.isInputShutdown() && (inputClosedSeenErrorOnRead || !isAllowHalfClosure(config)); + } + + private static boolean isAllowHalfClosure(ChannelConfig config) { + if (config instanceof EpollDomainSocketChannelConfig) { + return ((EpollDomainSocketChannelConfig) config).isAllowHalfClosure(); + } + return config instanceof SocketChannelConfig && + ((SocketChannelConfig) config).isAllowHalfClosure(); + } + + final void clearEpollIn() { + // Only clear if registered with an EventLoop as otherwise + if (isRegistered()) { + final EventLoop loop = eventLoop(); + final AbstractEpollUnsafe unsafe = (AbstractEpollUnsafe) unsafe(); + if (loop.inEventLoop()) { + unsafe.clearEpollIn0(); + } else { + // schedule a task to clear the EPOLLIN as it is not safe to modify it directly + loop.execute(new Runnable() { + @Override + public void run() { + if (!unsafe.readPending && !config().isAutoRead()) { + // Still no read triggered so clear it now + unsafe.clearEpollIn0(); + } + } + }); + } + } else { + // The EventLoop is not registered atm so just update the flags so the correct value + // will be used once the channel is registered + flags &= ~Native.EPOLLIN; + } + } + + private void modifyEvents() throws IOException { + if (isOpen() && isRegistered()) { + ((EpollEventLoop) eventLoop()).modify(this); + } + } + + @Override + protected void doRegister() throws Exception { + // Just in case the previous EventLoop was shutdown abruptly, or an event is still pending on the old EventLoop + // make sure the epollInReadyRunnablePending variable is reset so we will be able to execute the Runnable on the + // new EventLoop. + epollInReadyRunnablePending = false; + ((EpollEventLoop) eventLoop()).add(this); + } + + @Override + protected abstract AbstractEpollUnsafe newUnsafe(); + + /** + * Returns an off-heap copy of the specified {@link ByteBuf}, and releases the original one. + */ + protected final ByteBuf newDirectBuffer(ByteBuf buf) { + return newDirectBuffer(buf, buf); + } + + /** + * Returns an off-heap copy of the specified {@link ByteBuf}, and releases the specified holder. + * The caller must ensure that the holder releases the original {@link ByteBuf} when the holder is released by + * this method. + */ + protected final ByteBuf newDirectBuffer(Object holder, ByteBuf buf) { + final int readableBytes = buf.readableBytes(); + if (readableBytes == 0) { + ReferenceCountUtil.release(holder); + return Unpooled.EMPTY_BUFFER; + } + + final ByteBufAllocator alloc = alloc(); + if (alloc.isDirectBufferPooled()) { + return newDirectBuffer0(holder, buf, alloc, readableBytes); + } + + final ByteBuf directBuf = ByteBufUtil.threadLocalDirectBuffer(); + if (directBuf == null) { + return newDirectBuffer0(holder, buf, alloc, readableBytes); + } + + directBuf.writeBytes(buf, buf.readerIndex(), readableBytes); + ReferenceCountUtil.safeRelease(holder); + return directBuf; + } + + private static ByteBuf newDirectBuffer0(Object holder, ByteBuf buf, ByteBufAllocator alloc, int capacity) { + final ByteBuf directBuf = alloc.directBuffer(capacity); + directBuf.writeBytes(buf, buf.readerIndex(), capacity); + ReferenceCountUtil.safeRelease(holder); + return directBuf; + } + + protected static void checkResolvable(InetSocketAddress addr) { + if (addr.isUnresolved()) { + throw new UnresolvedAddressException(); + } + } + + /** + * Read bytes into the given {@link ByteBuf} and return the amount. + */ + protected final int doReadBytes(ByteBuf byteBuf) throws Exception { + int writerIndex = byteBuf.writerIndex(); + int localReadAmount; + unsafe().recvBufAllocHandle().attemptedBytesRead(byteBuf.writableBytes()); + if (byteBuf.hasMemoryAddress()) { + localReadAmount = socket.recvAddress(byteBuf.memoryAddress(), writerIndex, byteBuf.capacity()); + } else { + ByteBuffer buf = byteBuf.internalNioBuffer(writerIndex, byteBuf.writableBytes()); + localReadAmount = socket.recv(buf, buf.position(), buf.limit()); + } + if (localReadAmount > 0) { + byteBuf.writerIndex(writerIndex + localReadAmount); + } + return localReadAmount; + } + + protected final int doWriteBytes(ChannelOutboundBuffer in, ByteBuf buf) throws Exception { + if (buf.hasMemoryAddress()) { + int localFlushedAmount = socket.sendAddress(buf.memoryAddress(), buf.readerIndex(), buf.writerIndex()); + if (localFlushedAmount > 0) { + in.removeBytes(localFlushedAmount); + return 1; + } + } else { + final ByteBuffer nioBuf = buf.nioBufferCount() == 1 ? + buf.internalNioBuffer(buf.readerIndex(), buf.readableBytes()) : buf.nioBuffer(); + int localFlushedAmount = socket.send(nioBuf, nioBuf.position(), nioBuf.limit()); + if (localFlushedAmount > 0) { + nioBuf.position(nioBuf.position() + localFlushedAmount); + in.removeBytes(localFlushedAmount); + return 1; + } + } + return WRITE_STATUS_SNDBUF_FULL; + } + + /** + * Write bytes to the socket, with or without a remote address. + * Used for datagram and TCP client fast open writes. + */ + final long doWriteOrSendBytes(ByteBuf data, InetSocketAddress remoteAddress, boolean fastOpen) + throws IOException { + assert !(fastOpen && remoteAddress == null) : "fastOpen requires a remote address"; + if (data.hasMemoryAddress()) { + long memoryAddress = data.memoryAddress(); + if (remoteAddress == null) { + return socket.sendAddress(memoryAddress, data.readerIndex(), data.writerIndex()); + } + return socket.sendToAddress(memoryAddress, data.readerIndex(), data.writerIndex(), + remoteAddress.getAddress(), remoteAddress.getPort(), fastOpen); + } + + if (data.nioBufferCount() > 1) { + IovArray array = ((EpollEventLoop) eventLoop()).cleanIovArray(); + array.add(data, data.readerIndex(), data.readableBytes()); + int cnt = array.count(); + assert cnt != 0; + + if (remoteAddress == null) { + return socket.writevAddresses(array.memoryAddress(0), cnt); + } + return socket.sendToAddresses(array.memoryAddress(0), cnt, + remoteAddress.getAddress(), remoteAddress.getPort(), fastOpen); + } + + ByteBuffer nioData = data.internalNioBuffer(data.readerIndex(), data.readableBytes()); + if (remoteAddress == null) { + return socket.send(nioData, nioData.position(), nioData.limit()); + } + return socket.sendTo(nioData, nioData.position(), nioData.limit(), + remoteAddress.getAddress(), remoteAddress.getPort(), fastOpen); + } + + protected abstract class AbstractEpollUnsafe extends AbstractUnsafe { + boolean readPending; + boolean maybeMoreDataToRead; + private EpollRecvByteAllocatorHandle allocHandle; + private final Runnable epollInReadyRunnable = new Runnable() { + @Override + public void run() { + epollInReadyRunnablePending = false; + epollInReady(); + } + }; + + /** + * Called once EPOLLIN event is ready to be processed + */ + abstract void epollInReady(); + + final void epollInBefore() { + maybeMoreDataToRead = false; + } + + final void epollInFinally(ChannelConfig config) { + maybeMoreDataToRead = allocHandle.maybeMoreDataToRead(); + + if (allocHandle.isReceivedRdHup() || (readPending && maybeMoreDataToRead)) { + // trigger a read again as there may be something left to read and because of epoll ET we + // will not get notified again until we read everything from the socket + // + // It is possible the last fireChannelRead call could cause the user to call read() again, or if + // autoRead is true the call to channelReadComplete would also call read, but maybeMoreDataToRead is set + // to false before every read operation to prevent re-entry into epollInReady() we will not read from + // the underlying OS again unless the user happens to call read again. + executeEpollInReadyRunnable(config); + } else if (!readPending && !config.isAutoRead()) { + // Check if there is a readPending which was not processed yet. + // This could be for two reasons: + // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method + // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method + // + // See https://github.com/netty/netty/issues/2254 + clearEpollIn(); + } + } + + final void executeEpollInReadyRunnable(ChannelConfig config) { + if (epollInReadyRunnablePending || !isActive() || shouldBreakEpollInReady(config)) { + return; + } + epollInReadyRunnablePending = true; + eventLoop().execute(epollInReadyRunnable); + } + + /** + * Called once EPOLLRDHUP event is ready to be processed + */ + final void epollRdHupReady() { + // This must happen before we attempt to read. This will ensure reading continues until an error occurs. + recvBufAllocHandle().receivedRdHup(); + + if (isActive()) { + // If it is still active, we need to call epollInReady as otherwise we may miss to + // read pending data from the underlying file descriptor. + // See https://github.com/netty/netty/issues/3709 + epollInReady(); + } else { + // Just to be safe make sure the input marked as closed. + shutdownInput(true); + } + + // Clear the EPOLLRDHUP flag to prevent continuously getting woken up on this event. + clearEpollRdHup(); + } + + /** + * Clear the {@link Native#EPOLLRDHUP} flag from EPOLL, and close on failure. + */ + private void clearEpollRdHup() { + try { + clearFlag(Native.EPOLLRDHUP); + } catch (IOException e) { + pipeline().fireExceptionCaught(e); + close(voidPromise()); + } + } + + /** + * Shutdown the input side of the channel. + */ + void shutdownInput(boolean rdHup) { + if (!socket.isInputShutdown()) { + if (isAllowHalfClosure(config())) { + try { + socket.shutdown(true, false); + } catch (IOException ignored) { + // We attempted to shutdown and failed, which means the input has already effectively been + // shutdown. + fireEventAndClose(ChannelInputShutdownEvent.INSTANCE); + return; + } catch (NotYetConnectedException ignore) { + // We attempted to shutdown and failed, which means the input has already effectively been + // shutdown. + } + clearEpollIn0(); + pipeline().fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE); + } else { + close(voidPromise()); + } + } else if (!rdHup && !inputClosedSeenErrorOnRead) { + inputClosedSeenErrorOnRead = true; + pipeline().fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE); + } + } + + private void fireEventAndClose(Object evt) { + pipeline().fireUserEventTriggered(evt); + close(voidPromise()); + } + + @Override + public EpollRecvByteAllocatorHandle recvBufAllocHandle() { + if (allocHandle == null) { + allocHandle = newEpollHandle((RecvByteBufAllocator.ExtendedHandle) super.recvBufAllocHandle()); + } + return allocHandle; + } + + /** + * Create a new {@link EpollRecvByteAllocatorHandle} instance. + * @param handle The handle to wrap with EPOLL specific logic. + */ + EpollRecvByteAllocatorHandle newEpollHandle(RecvByteBufAllocator.ExtendedHandle handle) { + return new EpollRecvByteAllocatorHandle(handle); + } + + @Override + protected final void flush0() { + // Flush immediately only when there's no pending flush. + // If there's a pending flush operation, event loop will call forceFlush() later, + // and thus there's no need to call it now. + if (!isFlagSet(Native.EPOLLOUT)) { + super.flush0(); + } + } + + /** + * Called once a EPOLLOUT event is ready to be processed + */ + final void epollOutReady() { + if (connectPromise != null) { + // pending connect which is now complete so handle it. + finishConnect(); + } else if (!socket.isOutputShutdown()) { + // directly call super.flush0() to force a flush now + super.flush0(); + } + } + + protected final void clearEpollIn0() { + assert eventLoop().inEventLoop(); + try { + readPending = false; + clearFlag(Native.EPOLLIN); + } catch (IOException e) { + // When this happens there is something completely wrong with either the filedescriptor or epoll, + // so fire the exception through the pipeline and close the Channel. + pipeline().fireExceptionCaught(e); + unsafe().close(unsafe().voidPromise()); + } + } + + @Override + public void connect( + final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { + if (!promise.setUncancellable() || !ensureOpen(promise)) { + return; + } + + try { + if (connectPromise != null) { + throw new ConnectionPendingException(); + } + + boolean wasActive = isActive(); + if (doConnect(remoteAddress, localAddress)) { + fulfillConnectPromise(promise, wasActive); + } else { + connectPromise = promise; + requestedRemoteAddress = remoteAddress; + + // Schedule connect timeout. + final int connectTimeoutMillis = config().getConnectTimeoutMillis(); + if (connectTimeoutMillis > 0) { + connectTimeoutFuture = eventLoop().schedule(new Runnable() { + @Override + public void run() { + ChannelPromise connectPromise = AbstractEpollChannel.this.connectPromise; + if (connectPromise != null && !connectPromise.isDone() + && connectPromise.tryFailure(new ConnectTimeoutException( + "connection timed out after " + connectTimeoutMillis + " ms: " + + remoteAddress))) { + close(voidPromise()); + } + } + }, connectTimeoutMillis, TimeUnit.MILLISECONDS); + } + + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isCancelled()) { + if (connectTimeoutFuture != null) { + connectTimeoutFuture.cancel(false); + } + connectPromise = null; + close(voidPromise()); + } + } + }); + } + } catch (Throwable t) { + closeIfClosed(); + promise.tryFailure(annotateConnectException(t, remoteAddress)); + } + } + + private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) { + if (promise == null) { + // Closed via cancellation and the promise has been notified already. + return; + } + active = true; + + // Get the state as trySuccess() may trigger an ChannelFutureListener that will close the Channel. + // We still need to ensure we call fireChannelActive() in this case. + boolean active = isActive(); + + // trySuccess() will return false if a user cancelled the connection attempt. + boolean promiseSet = promise.trySuccess(); + + // Regardless if the connection attempt was cancelled, channelActive() event should be triggered, + // because what happened is what happened. + if (!wasActive && active) { + pipeline().fireChannelActive(); + } + + // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive(). + if (!promiseSet) { + close(voidPromise()); + } + } + + private void fulfillConnectPromise(ChannelPromise promise, Throwable cause) { + if (promise == null) { + // Closed via cancellation and the promise has been notified already. + return; + } + + // Use tryFailure() instead of setFailure() to avoid the race against cancel(). + promise.tryFailure(cause); + closeIfClosed(); + } + + private void finishConnect() { + // Note this method is invoked by the event loop only if the connection attempt was + // neither cancelled nor timed out. + + assert eventLoop().inEventLoop(); + + boolean connectStillInProgress = false; + try { + boolean wasActive = isActive(); + if (!doFinishConnect()) { + connectStillInProgress = true; + return; + } + fulfillConnectPromise(connectPromise, wasActive); + } catch (Throwable t) { + fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress)); + } finally { + if (!connectStillInProgress) { + // Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used + // See https://github.com/netty/netty/issues/1770 + if (connectTimeoutFuture != null) { + connectTimeoutFuture.cancel(false); + } + connectPromise = null; + } + } + } + + /** + * Finish the connect + */ + private boolean doFinishConnect() throws Exception { + if (socket.finishConnect()) { + clearFlag(Native.EPOLLOUT); + if (requestedRemoteAddress instanceof InetSocketAddress) { + remote = computeRemoteAddr((InetSocketAddress) requestedRemoteAddress, socket.remoteAddress()); + } + requestedRemoteAddress = null; + + return true; + } + setFlag(Native.EPOLLOUT); + return false; + } + } + + @Override + protected void doBind(SocketAddress local) throws Exception { + if (local instanceof InetSocketAddress) { + checkResolvable((InetSocketAddress) local); + } + socket.bind(local); + this.local = socket.localAddress(); + } + + /** + * Connect to the remote peer + */ + protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + if (localAddress instanceof InetSocketAddress) { + checkResolvable((InetSocketAddress) localAddress); + } + + InetSocketAddress remoteSocketAddr = remoteAddress instanceof InetSocketAddress + ? (InetSocketAddress) remoteAddress : null; + if (remoteSocketAddr != null) { + checkResolvable(remoteSocketAddr); + } + + if (remote != null) { + // Check if already connected before trying to connect. This is needed as connect(...) will not return -1 + // and set errno to EISCONN if a previous connect(...) attempt was setting errno to EINPROGRESS and finished + // later. + throw new AlreadyConnectedException(); + } + + if (localAddress != null) { + socket.bind(localAddress); + } + + boolean connected = doConnect0(remoteAddress); + if (connected) { + remote = remoteSocketAddr == null ? + remoteAddress : computeRemoteAddr(remoteSocketAddr, socket.remoteAddress()); + } + // We always need to set the localAddress even if not connected yet as the bind already took place. + // + // See https://github.com/netty/netty/issues/3463 + local = socket.localAddress(); + return connected; + } + + boolean doConnect0(SocketAddress remote) throws Exception { + boolean success = false; + try { + boolean connected = socket.connect(remote); + if (!connected) { + setFlag(Native.EPOLLOUT); + } + success = true; + return connected; + } finally { + if (!success) { + doClose(); + } + } + } + + @Override + protected SocketAddress localAddress0() { + return local; + } + + @Override + protected SocketAddress remoteAddress0() { + return remote; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollServerChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollServerChannel.java new file mode 100644 index 0000000..c4ea86f --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollServerChannel.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.channel.ServerChannel; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public abstract class AbstractEpollServerChannel extends AbstractEpollChannel implements ServerChannel { + private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); + + protected AbstractEpollServerChannel(int fd) { + this(new LinuxSocket(fd), false); + } + + protected AbstractEpollServerChannel(LinuxSocket fd) { + this(fd, isSoErrorZero(fd)); + } + + protected AbstractEpollServerChannel(LinuxSocket fd, boolean active) { + super(null, fd, active); + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + protected boolean isCompatible(EventLoop loop) { + return loop instanceof EpollEventLoop; + } + + @Override + protected InetSocketAddress remoteAddress0() { + return null; + } + + @Override + protected AbstractEpollUnsafe newUnsafe() { + return new EpollServerSocketUnsafe(); + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + protected Object filterOutboundMessage(Object msg) throws Exception { + throw new UnsupportedOperationException(); + } + + protected abstract Channel newChildChannel(int fd, byte[] remote, int offset, int len) throws Exception; + + final class EpollServerSocketUnsafe extends AbstractEpollUnsafe { + // Will hold the remote address after accept(...) was successful. + // We need 24 bytes for the address as maximum + 1 byte for storing the length. + // So use 26 bytes as it's a power of two. + private final byte[] acceptedAddress = new byte[26]; + + @Override + public void connect(SocketAddress socketAddress, SocketAddress socketAddress2, ChannelPromise channelPromise) { + // Connect not supported by ServerChannel implementations + channelPromise.setFailure(new UnsupportedOperationException()); + } + + @Override + void epollInReady() { + assert eventLoop().inEventLoop(); + final ChannelConfig config = config(); + if (shouldBreakEpollInReady(config)) { + clearEpollIn0(); + return; + } + final EpollRecvByteAllocatorHandle allocHandle = recvBufAllocHandle(); + allocHandle.edgeTriggered(isFlagSet(Native.EPOLLET)); + + final ChannelPipeline pipeline = pipeline(); + allocHandle.reset(config); + allocHandle.attemptedBytesRead(1); + epollInBefore(); + + Throwable exception = null; + try { + try { + do { + // lastBytesRead represents the fd. We use lastBytesRead because it must be set so that the + // EpollRecvByteAllocatorHandle knows if it should try to read again or not when autoRead is + // enabled. + allocHandle.lastBytesRead(socket.accept(acceptedAddress)); + if (allocHandle.lastBytesRead() == -1) { + // this means everything was handled for now + break; + } + allocHandle.incMessagesRead(1); + + readPending = false; + pipeline.fireChannelRead(newChildChannel(allocHandle.lastBytesRead(), acceptedAddress, 1, + acceptedAddress[0])); + } while (allocHandle.continueReading()); + } catch (Throwable t) { + exception = t; + } + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + + if (exception != null) { + pipeline.fireExceptionCaught(exception); + } + } finally { + epollInFinally(config); + } + } + } + + @Override + protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + throw new UnsupportedOperationException(); + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java new file mode 100644 index 0000000..7cbe0d1 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java @@ -0,0 +1,1059 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultFileRegion; +import io.netty.channel.EventLoop; +import io.netty.channel.FileRegion; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.internal.ChannelUtils; +import io.netty.channel.socket.DuplexChannel; +import io.netty.channel.unix.FileDescriptor; +import io.netty.channel.unix.IovArray; +import io.netty.channel.unix.SocketWritableByteChannel; +import io.netty.channel.unix.UnixChannelUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.WritableByteChannel; +import java.util.Queue; +import java.util.concurrent.Executor; + +import static io.netty.channel.internal.ChannelUtils.MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD; +import static io.netty.channel.internal.ChannelUtils.WRITE_STATUS_SNDBUF_FULL; +import static io.netty.channel.unix.FileDescriptor.pipe; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel implements DuplexChannel { + private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); + private static final String EXPECTED_TYPES = + " (expected: " + StringUtil.simpleClassName(ByteBuf.class) + ", " + + StringUtil.simpleClassName(DefaultFileRegion.class) + ')'; + private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractEpollStreamChannel.class); + + private final Runnable flushTask = new Runnable() { + @Override + public void run() { + // Calling flush0 directly to ensure we not try to flush messages that were added via write(...) in the + // meantime. + ((AbstractEpollUnsafe) unsafe()).flush0(); + } + }; + + // Lazy init these if we need to splice(...) + private volatile Queue spliceQueue; + private FileDescriptor pipeIn; + private FileDescriptor pipeOut; + + private WritableByteChannel byteChannel; + + protected AbstractEpollStreamChannel(Channel parent, int fd) { + this(parent, new LinuxSocket(fd)); + } + + protected AbstractEpollStreamChannel(int fd) { + this(new LinuxSocket(fd)); + } + + AbstractEpollStreamChannel(LinuxSocket fd) { + this(fd, isSoErrorZero(fd)); + } + + AbstractEpollStreamChannel(Channel parent, LinuxSocket fd) { + super(parent, fd, true); + // Add EPOLLRDHUP so we are notified once the remote peer close the connection. + flags |= Native.EPOLLRDHUP; + } + + protected AbstractEpollStreamChannel(Channel parent, LinuxSocket fd, SocketAddress remote) { + super(parent, fd, remote); + // Add EPOLLRDHUP so we are notified once the remote peer close the connection. + flags |= Native.EPOLLRDHUP; + } + + protected AbstractEpollStreamChannel(LinuxSocket fd, boolean active) { + super(null, fd, active); + // Add EPOLLRDHUP so we are notified once the remote peer close the connection. + flags |= Native.EPOLLRDHUP; + } + + @Override + protected AbstractEpollUnsafe newUnsafe() { + return new EpollStreamUnsafe(); + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + /** + * Splice from this {@link AbstractEpollStreamChannel} to another {@link AbstractEpollStreamChannel}. + * The {@code len} is the number of bytes to splice. If using {@link Integer#MAX_VALUE} it will + * splice until the {@link ChannelFuture} was canceled or it was failed. + * + * Please note: + * + * + */ + public final ChannelFuture spliceTo(final AbstractEpollStreamChannel ch, final int len) { + return spliceTo(ch, len, newPromise()); + } + + /** + * Splice from this {@link AbstractEpollStreamChannel} to another {@link AbstractEpollStreamChannel}. + * The {@code len} is the number of bytes to splice. If using {@link Integer#MAX_VALUE} it will + * splice until the {@link ChannelFuture} was canceled or it was failed. + * + * Please note: + * + * + */ + public final ChannelFuture spliceTo(final AbstractEpollStreamChannel ch, final int len, + final ChannelPromise promise) { + if (ch.eventLoop() != eventLoop()) { + throw new IllegalArgumentException("EventLoops are not the same."); + } + checkPositiveOrZero(len, "len"); + if (ch.config().getEpollMode() != EpollMode.LEVEL_TRIGGERED + || config().getEpollMode() != EpollMode.LEVEL_TRIGGERED) { + throw new IllegalStateException("spliceTo() supported only when using " + EpollMode.LEVEL_TRIGGERED); + } + checkNotNull(promise, "promise"); + if (!isOpen()) { + promise.tryFailure(new ClosedChannelException()); + } else { + addToSpliceQueue(new SpliceInChannelTask(ch, len, promise)); + failSpliceIfClosed(promise); + } + return promise; + } + + /** + * Splice from this {@link AbstractEpollStreamChannel} to another {@link FileDescriptor}. + * The {@code offset} is the offset for the {@link FileDescriptor} and {@code len} is the + * number of bytes to splice. If using {@link Integer#MAX_VALUE} it will splice until the + * {@link ChannelFuture} was canceled or it was failed. + * + * Please note: + * + */ + public final ChannelFuture spliceTo(final FileDescriptor ch, final int offset, final int len) { + return spliceTo(ch, offset, len, newPromise()); + } + + /** + * Splice from this {@link AbstractEpollStreamChannel} to another {@link FileDescriptor}. + * The {@code offset} is the offset for the {@link FileDescriptor} and {@code len} is the + * number of bytes to splice. If using {@link Integer#MAX_VALUE} it will splice until the + * {@link ChannelFuture} was canceled or it was failed. + * + * Please note: + * + */ + public final ChannelFuture spliceTo(final FileDescriptor ch, final int offset, final int len, + final ChannelPromise promise) { + checkPositiveOrZero(len, "len"); + checkPositiveOrZero(offset, "offset"); + if (config().getEpollMode() != EpollMode.LEVEL_TRIGGERED) { + throw new IllegalStateException("spliceTo() supported only when using " + EpollMode.LEVEL_TRIGGERED); + } + checkNotNull(promise, "promise"); + if (!isOpen()) { + promise.tryFailure(new ClosedChannelException()); + } else { + addToSpliceQueue(new SpliceFdTask(ch, offset, len, promise)); + failSpliceIfClosed(promise); + } + return promise; + } + + private void failSpliceIfClosed(ChannelPromise promise) { + if (!isOpen()) { + // Seems like the Channel was closed in the meantime try to fail the promise to prevent any + // cases where a future may not be notified otherwise. + if (promise.tryFailure(new ClosedChannelException())) { + eventLoop().execute(new Runnable() { + @Override + public void run() { + // Call this via the EventLoop as it is a MPSC queue. + clearSpliceQueue(); + } + }); + } + } + } + + /** + * Write bytes form the given {@link ByteBuf} to the underlying {@link java.nio.channels.Channel}. + * @param in the collection which contains objects to write. + * @param buf the {@link ByteBuf} from which the bytes should be written + * @return The value that should be decremented from the write quantum which starts at + * {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows: + * + */ + private int writeBytes(ChannelOutboundBuffer in, ByteBuf buf) throws Exception { + int readableBytes = buf.readableBytes(); + if (readableBytes == 0) { + in.remove(); + return 0; + } + + if (buf.hasMemoryAddress() || buf.nioBufferCount() == 1) { + return doWriteBytes(in, buf); + } else { + ByteBuffer[] nioBuffers = buf.nioBuffers(); + return writeBytesMultiple(in, nioBuffers, nioBuffers.length, readableBytes, + config().getMaxBytesPerGatheringWrite()); + } + } + + private void adjustMaxBytesPerGatheringWrite(long attempted, long written, long oldMaxBytesPerGatheringWrite) { + // By default we track the SO_SNDBUF when ever it is explicitly set. However some OSes may dynamically change + // SO_SNDBUF (and other characteristics that determine how much data can be written at once) so we should try + // make a best effort to adjust as OS behavior changes. + if (attempted == written) { + if (attempted << 1 > oldMaxBytesPerGatheringWrite) { + config().setMaxBytesPerGatheringWrite(attempted << 1); + } + } else if (attempted > MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD && written < attempted >>> 1) { + config().setMaxBytesPerGatheringWrite(attempted >>> 1); + } + } + + /** + * Write multiple bytes via {@link IovArray}. + * @param in the collection which contains objects to write. + * @param array The array which contains the content to write. + * @return The value that should be decremented from the write quantum which starts at + * {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows: + * + * @throws IOException If an I/O exception occurs during write. + */ + private int writeBytesMultiple(ChannelOutboundBuffer in, IovArray array) throws IOException { + final long expectedWrittenBytes = array.size(); + assert expectedWrittenBytes != 0; + final int cnt = array.count(); + assert cnt != 0; + + final long localWrittenBytes = socket.writevAddresses(array.memoryAddress(0), cnt); + if (localWrittenBytes > 0) { + adjustMaxBytesPerGatheringWrite(expectedWrittenBytes, localWrittenBytes, array.maxBytes()); + in.removeBytes(localWrittenBytes); + return 1; + } + return WRITE_STATUS_SNDBUF_FULL; + } + + /** + * Write multiple bytes via {@link ByteBuffer} array. + * @param in the collection which contains objects to write. + * @param nioBuffers The buffers to write. + * @param nioBufferCnt The number of buffers to write. + * @param expectedWrittenBytes The number of bytes we expect to write. + * @param maxBytesPerGatheringWrite The maximum number of bytes we should attempt to write. + * @return The value that should be decremented from the write quantum which starts at + * {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows: + * + * @throws IOException If an I/O exception occurs during write. + */ + private int writeBytesMultiple( + ChannelOutboundBuffer in, ByteBuffer[] nioBuffers, int nioBufferCnt, long expectedWrittenBytes, + long maxBytesPerGatheringWrite) throws IOException { + assert expectedWrittenBytes != 0; + if (expectedWrittenBytes > maxBytesPerGatheringWrite) { + expectedWrittenBytes = maxBytesPerGatheringWrite; + } + + final long localWrittenBytes = socket.writev(nioBuffers, 0, nioBufferCnt, expectedWrittenBytes); + if (localWrittenBytes > 0) { + adjustMaxBytesPerGatheringWrite(expectedWrittenBytes, localWrittenBytes, maxBytesPerGatheringWrite); + in.removeBytes(localWrittenBytes); + return 1; + } + return WRITE_STATUS_SNDBUF_FULL; + } + + /** + * Write a {@link DefaultFileRegion} + * @param in the collection which contains objects to write. + * @param region the {@link DefaultFileRegion} from which the bytes should be written + * @return The value that should be decremented from the write quantum which starts at + * {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows: + * + */ + private int writeDefaultFileRegion(ChannelOutboundBuffer in, DefaultFileRegion region) throws Exception { + final long offset = region.transferred(); + final long regionCount = region.count(); + if (offset >= regionCount) { + in.remove(); + return 0; + } + + final long flushedAmount = socket.sendFile(region, region.position(), offset, regionCount - offset); + if (flushedAmount > 0) { + in.progress(flushedAmount); + if (region.transferred() >= regionCount) { + in.remove(); + } + return 1; + } else if (flushedAmount == 0) { + validateFileRegion(region, offset); + } + return WRITE_STATUS_SNDBUF_FULL; + } + + /** + * Write a {@link FileRegion} + * @param in the collection which contains objects to write. + * @param region the {@link FileRegion} from which the bytes should be written + * @return The value that should be decremented from the write quantum which starts at + * {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows: + * + */ + private int writeFileRegion(ChannelOutboundBuffer in, FileRegion region) throws Exception { + if (region.transferred() >= region.count()) { + in.remove(); + return 0; + } + + if (byteChannel == null) { + byteChannel = new EpollSocketWritableByteChannel(); + } + final long flushedAmount = region.transferTo(byteChannel, region.transferred()); + if (flushedAmount > 0) { + in.progress(flushedAmount); + if (region.transferred() >= region.count()) { + in.remove(); + } + return 1; + } + return WRITE_STATUS_SNDBUF_FULL; + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + int writeSpinCount = config().getWriteSpinCount(); + do { + final int msgCount = in.size(); + // Do gathering write if the outbound buffer entries start with more than one ByteBuf. + if (msgCount > 1 && in.current() instanceof ByteBuf) { + writeSpinCount -= doWriteMultiple(in); + } else if (msgCount == 0) { + // Wrote all messages. + clearFlag(Native.EPOLLOUT); + // Return here so we not set the EPOLLOUT flag. + return; + } else { // msgCount == 1 + writeSpinCount -= doWriteSingle(in); + } + + // We do not break the loop here even if the outbound buffer was flushed completely, + // because a user might have triggered another write and flush when we notify his or her + // listeners. + } while (writeSpinCount > 0); + + if (writeSpinCount == 0) { + // It is possible that we have set EPOLLOUT, woken up by EPOLL because the socket is writable, and then use + // our write quantum. In this case we no longer want to set the EPOLLOUT flag because the socket is still + // writable (as far as we know). We will find out next time we attempt to write if the socket is writable + // and set the EPOLLOUT if necessary. + clearFlag(Native.EPOLLOUT); + + // We used our writeSpin quantum, and should try to write again later. + eventLoop().execute(flushTask); + } else { + // Underlying descriptor can not accept all data currently, so set the EPOLLOUT flag to be woken up + // when it can accept more data. + setFlag(Native.EPOLLOUT); + } + } + + /** + * Attempt to write a single object. + * @param in the collection which contains objects to write. + * @return The value that should be decremented from the write quantum which starts at + * {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows: + * + * @throws Exception If an I/O error occurs. + */ + protected int doWriteSingle(ChannelOutboundBuffer in) throws Exception { + // The outbound buffer contains only one message or it contains a file region. + Object msg = in.current(); + if (msg instanceof ByteBuf) { + return writeBytes(in, (ByteBuf) msg); + } else if (msg instanceof DefaultFileRegion) { + return writeDefaultFileRegion(in, (DefaultFileRegion) msg); + } else if (msg instanceof FileRegion) { + return writeFileRegion(in, (FileRegion) msg); + } else if (msg instanceof SpliceOutTask) { + if (!((SpliceOutTask) msg).spliceOut()) { + return WRITE_STATUS_SNDBUF_FULL; + } + in.remove(); + return 1; + } else { + // Should never reach here. + throw new Error(); + } + } + + /** + * Attempt to write multiple {@link ByteBuf} objects. + * @param in the collection which contains objects to write. + * @return The value that should be decremented from the write quantum which starts at + * {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows: + * + * @throws Exception If an I/O error occurs. + */ + private int doWriteMultiple(ChannelOutboundBuffer in) throws Exception { + final long maxBytesPerGatheringWrite = config().getMaxBytesPerGatheringWrite(); + IovArray array = ((EpollEventLoop) eventLoop()).cleanIovArray(); + array.maxBytes(maxBytesPerGatheringWrite); + in.forEachFlushedMessage(array); + + if (array.count() >= 1) { + // TODO: Handle the case where cnt == 1 specially. + return writeBytesMultiple(in, array); + } + // cnt == 0, which means the outbound buffer contained empty buffers only. + in.removeBytes(0); + return 0; + } + + @Override + protected Object filterOutboundMessage(Object msg) { + if (msg instanceof ByteBuf) { + ByteBuf buf = (ByteBuf) msg; + return UnixChannelUtil.isBufferCopyNeededForWrite(buf)? newDirectBuffer(buf): buf; + } + + if (msg instanceof FileRegion || msg instanceof SpliceOutTask) { + return msg; + } + + throw new UnsupportedOperationException( + "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES); + } + + @UnstableApi + @Override + protected final void doShutdownOutput() throws Exception { + socket.shutdown(false, true); + } + + private void shutdownInput0(final ChannelPromise promise) { + try { + socket.shutdown(true, false); + promise.setSuccess(); + } catch (Throwable cause) { + promise.setFailure(cause); + } + } + + @Override + public boolean isOutputShutdown() { + return socket.isOutputShutdown(); + } + + @Override + public boolean isInputShutdown() { + return socket.isInputShutdown(); + } + + @Override + public boolean isShutdown() { + return socket.isShutdown(); + } + + @Override + public ChannelFuture shutdownOutput() { + return shutdownOutput(newPromise()); + } + + @Override + public ChannelFuture shutdownOutput(final ChannelPromise promise) { + EventLoop loop = eventLoop(); + if (loop.inEventLoop()) { + ((AbstractUnsafe) unsafe()).shutdownOutput(promise); + } else { + loop.execute(new Runnable() { + @Override + public void run() { + ((AbstractUnsafe) unsafe()).shutdownOutput(promise); + } + }); + } + + return promise; + } + + @Override + public ChannelFuture shutdownInput() { + return shutdownInput(newPromise()); + } + + @Override + public ChannelFuture shutdownInput(final ChannelPromise promise) { + Executor closeExecutor = ((EpollStreamUnsafe) unsafe()).prepareToClose(); + if (closeExecutor != null) { + closeExecutor.execute(new Runnable() { + @Override + public void run() { + shutdownInput0(promise); + } + }); + } else { + EventLoop loop = eventLoop(); + if (loop.inEventLoop()) { + shutdownInput0(promise); + } else { + loop.execute(new Runnable() { + @Override + public void run() { + shutdownInput0(promise); + } + }); + } + } + return promise; + } + + @Override + public ChannelFuture shutdown() { + return shutdown(newPromise()); + } + + @Override + public ChannelFuture shutdown(final ChannelPromise promise) { + ChannelFuture shutdownOutputFuture = shutdownOutput(); + if (shutdownOutputFuture.isDone()) { + shutdownOutputDone(shutdownOutputFuture, promise); + } else { + shutdownOutputFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(final ChannelFuture shutdownOutputFuture) throws Exception { + shutdownOutputDone(shutdownOutputFuture, promise); + } + }); + } + return promise; + } + + private void shutdownOutputDone(final ChannelFuture shutdownOutputFuture, final ChannelPromise promise) { + ChannelFuture shutdownInputFuture = shutdownInput(); + if (shutdownInputFuture.isDone()) { + shutdownDone(shutdownOutputFuture, shutdownInputFuture, promise); + } else { + shutdownInputFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture shutdownInputFuture) throws Exception { + shutdownDone(shutdownOutputFuture, shutdownInputFuture, promise); + } + }); + } + } + + private static void shutdownDone(ChannelFuture shutdownOutputFuture, + ChannelFuture shutdownInputFuture, + ChannelPromise promise) { + Throwable shutdownOutputCause = shutdownOutputFuture.cause(); + Throwable shutdownInputCause = shutdownInputFuture.cause(); + if (shutdownOutputCause != null) { + if (shutdownInputCause != null) { + logger.debug("Exception suppressed because a previous exception occurred.", + shutdownInputCause); + } + promise.setFailure(shutdownOutputCause); + } else if (shutdownInputCause != null) { + promise.setFailure(shutdownInputCause); + } else { + promise.setSuccess(); + } + } + + @Override + protected void doClose() throws Exception { + try { + // Calling super.doClose() first so spliceTo(...) will fail on next call. + super.doClose(); + } finally { + safeClosePipe(pipeIn); + safeClosePipe(pipeOut); + clearSpliceQueue(); + } + } + + private void clearSpliceQueue() { + Queue sQueue = spliceQueue; + if (sQueue == null) { + return; + } + ClosedChannelException exception = null; + + for (;;) { + SpliceInTask task = sQueue.poll(); + if (task == null) { + break; + } + if (exception == null) { + exception = new ClosedChannelException(); + } + task.promise.tryFailure(exception); + } + } + + private static void safeClosePipe(FileDescriptor fd) { + if (fd != null) { + try { + fd.close(); + } catch (IOException e) { + logger.warn("Error while closing a pipe", e); + } + } + } + + class EpollStreamUnsafe extends AbstractEpollUnsafe { + // Overridden here just to be able to access this method from AbstractEpollStreamChannel + @Override + protected Executor prepareToClose() { + return super.prepareToClose(); + } + + private void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf, Throwable cause, boolean close, + EpollRecvByteAllocatorHandle allocHandle) { + if (byteBuf != null) { + if (byteBuf.isReadable()) { + readPending = false; + pipeline.fireChannelRead(byteBuf); + } else { + byteBuf.release(); + } + } + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + pipeline.fireExceptionCaught(cause); + + // If oom will close the read event, release connection. + // See https://github.com/netty/netty/issues/10434 + if (close || cause instanceof OutOfMemoryError || cause instanceof IOException) { + shutdownInput(false); + } + } + + @Override + EpollRecvByteAllocatorHandle newEpollHandle(RecvByteBufAllocator.ExtendedHandle handle) { + return new EpollRecvByteAllocatorStreamingHandle(handle); + } + + @Override + void epollInReady() { + final ChannelConfig config = config(); + if (shouldBreakEpollInReady(config)) { + clearEpollIn0(); + return; + } + final EpollRecvByteAllocatorHandle allocHandle = recvBufAllocHandle(); + allocHandle.edgeTriggered(isFlagSet(Native.EPOLLET)); + + final ChannelPipeline pipeline = pipeline(); + final ByteBufAllocator allocator = config.getAllocator(); + allocHandle.reset(config); + epollInBefore(); + + ByteBuf byteBuf = null; + boolean close = false; + Queue sQueue = null; + try { + do { + if (sQueue != null || (sQueue = spliceQueue) != null) { + SpliceInTask spliceTask = sQueue.peek(); + if (spliceTask != null) { + boolean spliceInResult = spliceTask.spliceIn(allocHandle); + + if (allocHandle.isReceivedRdHup()) { + shutdownInput(true); + } + if (spliceInResult) { + // We need to check if it is still active as if not we removed all SpliceTasks in + // doClose(...) + if (isActive()) { + sQueue.remove(); + } + continue; + } else { + break; + } + } + } + + // we use a direct buffer here as the native implementations only be able + // to handle direct buffers. + byteBuf = allocHandle.allocate(allocator); + allocHandle.lastBytesRead(doReadBytes(byteBuf)); + if (allocHandle.lastBytesRead() <= 0) { + // nothing was read, release the buffer. + byteBuf.release(); + byteBuf = null; + close = allocHandle.lastBytesRead() < 0; + if (close) { + // There is nothing left to read as we received an EOF. + readPending = false; + } + break; + } + allocHandle.incMessagesRead(1); + readPending = false; + pipeline.fireChannelRead(byteBuf); + byteBuf = null; + + if (shouldBreakEpollInReady(config)) { + // We need to do this for two reasons: + // + // - If the input was shutdown in between (which may be the case when the user did it in the + // fireChannelRead(...) method we should not try to read again to not produce any + // miss-leading exceptions. + // + // - If the user closes the channel we need to ensure we not try to read from it again as + // the filedescriptor may be re-used already by the OS if the system is handling a lot of + // concurrent connections and so needs a lot of filedescriptors. If not do this we risk + // reading data from a filedescriptor that belongs to another socket then the socket that + // was "wrapped" by this Channel implementation. + break; + } + } while (allocHandle.continueReading()); + + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + + if (close) { + shutdownInput(false); + } + } catch (Throwable t) { + handleReadException(pipeline, byteBuf, t, close, allocHandle); + } finally { + if (sQueue == null) { + epollInFinally(config); + } else { + if (!config.isAutoRead()) { + clearEpollIn(); + } + } + } + } + } + + private void addToSpliceQueue(final SpliceInTask task) { + Queue sQueue = spliceQueue; + if (sQueue == null) { + synchronized (this) { + sQueue = spliceQueue; + if (sQueue == null) { + spliceQueue = sQueue = PlatformDependent.newMpscQueue(); + } + } + } + sQueue.add(task); + } + + protected abstract class SpliceInTask { + final ChannelPromise promise; + int len; + + protected SpliceInTask(int len, ChannelPromise promise) { + this.promise = promise; + this.len = len; + } + + abstract boolean spliceIn(RecvByteBufAllocator.Handle handle); + + protected final int spliceIn(FileDescriptor pipeOut, RecvByteBufAllocator.Handle handle) throws IOException { + // calculate the maximum amount of data we are allowed to splice + int length = Math.min(handle.guess(), len); + int splicedIn = 0; + for (;;) { + // Splicing until there is nothing left to splice. + int localSplicedIn = Native.splice(socket.intValue(), -1, pipeOut.intValue(), -1, length); + handle.lastBytesRead(localSplicedIn); + if (localSplicedIn == 0) { + break; + } + splicedIn += localSplicedIn; + length -= localSplicedIn; + } + + return splicedIn; + } + } + + // Let it directly implement channelFutureListener as well to reduce object creation. + private final class SpliceInChannelTask extends SpliceInTask implements ChannelFutureListener { + private final AbstractEpollStreamChannel ch; + + SpliceInChannelTask(AbstractEpollStreamChannel ch, int len, ChannelPromise promise) { + super(len, promise); + this.ch = ch; + } + + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + promise.setFailure(future.cause()); + } + } + + @Override + public boolean spliceIn(RecvByteBufAllocator.Handle handle) { + assert ch.eventLoop().inEventLoop(); + if (len == 0) { + promise.setSuccess(); + return true; + } + try { + // We create the pipe on the target channel as this will allow us to just handle pending writes + // later in a correct fashion without get into any ordering issues when spliceTo(...) is called + // on multiple Channels pointing to one target Channel. + FileDescriptor pipeOut = ch.pipeOut; + if (pipeOut == null) { + // Create a new pipe as non was created before. + FileDescriptor[] pipe = pipe(); + ch.pipeIn = pipe[0]; + pipeOut = ch.pipeOut = pipe[1]; + } + + int splicedIn = spliceIn(pipeOut, handle); + if (splicedIn > 0) { + // Integer.MAX_VALUE is a special value which will result in splice forever. + if (len != Integer.MAX_VALUE) { + len -= splicedIn; + } + + // Depending on if we are done with splicing inbound data we set the right promise for the + // outbound splicing. + final ChannelPromise splicePromise; + if (len == 0) { + splicePromise = promise; + } else { + splicePromise = ch.newPromise().addListener(this); + } + + boolean autoRead = config().isAutoRead(); + + // Just call unsafe().write(...) and flush() as we not want to traverse the whole pipeline for this + // case. + ch.unsafe().write(new SpliceOutTask(ch, splicedIn, autoRead), splicePromise); + ch.unsafe().flush(); + if (autoRead && !splicePromise.isDone()) { + // Write was not done which means the target channel was not writable. In this case we need to + // disable reading until we are done with splicing to the target channel because: + // + // - The user may want to to trigger another splice operation once the splicing was complete. + config().setAutoRead(false); + } + } + + return len == 0; + } catch (Throwable cause) { + promise.setFailure(cause); + return true; + } + } + } + + private final class SpliceOutTask { + private final AbstractEpollStreamChannel ch; + private final boolean autoRead; + private int len; + + SpliceOutTask(AbstractEpollStreamChannel ch, int len, boolean autoRead) { + this.ch = ch; + this.len = len; + this.autoRead = autoRead; + } + + public boolean spliceOut() throws Exception { + assert ch.eventLoop().inEventLoop(); + try { + int splicedOut = Native.splice(ch.pipeIn.intValue(), -1, ch.socket.intValue(), -1, len); + len -= splicedOut; + if (len == 0) { + if (autoRead) { + // AutoRead was used and we spliced everything so start reading again + config().setAutoRead(true); + } + return true; + } + return false; + } catch (IOException e) { + if (autoRead) { + // AutoRead was used and we spliced everything so start reading again + config().setAutoRead(true); + } + throw e; + } + } + } + + private final class SpliceFdTask extends SpliceInTask { + private final FileDescriptor fd; + private final ChannelPromise promise; + private int offset; + + SpliceFdTask(FileDescriptor fd, int offset, int len, ChannelPromise promise) { + super(len, promise); + this.fd = fd; + this.promise = promise; + this.offset = offset; + } + + @Override + public boolean spliceIn(RecvByteBufAllocator.Handle handle) { + assert eventLoop().inEventLoop(); + if (len == 0) { + promise.setSuccess(); + return true; + } + + try { + FileDescriptor[] pipe = pipe(); + FileDescriptor pipeIn = pipe[0]; + FileDescriptor pipeOut = pipe[1]; + try { + int splicedIn = spliceIn(pipeOut, handle); + if (splicedIn > 0) { + // Integer.MAX_VALUE is a special value which will result in splice forever. + if (len != Integer.MAX_VALUE) { + len -= splicedIn; + } + do { + int splicedOut = Native.splice(pipeIn.intValue(), -1, fd.intValue(), offset, splicedIn); + offset += splicedOut; + splicedIn -= splicedOut; + } while (splicedIn > 0); + if (len == 0) { + promise.setSuccess(); + return true; + } + } + return false; + } finally { + safeClosePipe(pipeIn); + safeClosePipe(pipeOut); + } + } catch (Throwable cause) { + promise.setFailure(cause); + return true; + } + } + } + + private final class EpollSocketWritableByteChannel extends SocketWritableByteChannel { + EpollSocketWritableByteChannel() { + super(socket); + assert fd == socket; + } + + @Override + protected int write(final ByteBuffer buf, final int pos, final int limit) throws IOException { + return socket.send(buf, pos, limit); + } + + @Override + protected ByteBufAllocator alloc() { + return AbstractEpollStreamChannel.this.alloc(); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/Epoll.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/Epoll.java new file mode 100644 index 0000000..69620b1 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/Epoll.java @@ -0,0 +1,118 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.channel.ChannelOption; +import io.netty.channel.unix.FileDescriptor; +import io.netty.util.internal.SystemPropertyUtil; + +/** + * Tells if {@code netty-transport-native-epoll} is + * supported. + */ +public final class Epoll { + + private static final Throwable UNAVAILABILITY_CAUSE; + + static { + Throwable cause = null; + + if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) { + cause = new UnsupportedOperationException( + "Native transport was explicit disabled with -Dio.netty.transport.noNative=true"); + } else { + FileDescriptor epollFd = null; + FileDescriptor eventFd = null; + try { + epollFd = Native.newEpollCreate(); + eventFd = Native.newEventFd(); + } catch (Throwable t) { + cause = t; + } finally { + if (epollFd != null) { + try { + epollFd.close(); + } catch (Exception ignore) { + // ignore + } + } + if (eventFd != null) { + try { + eventFd.close(); + } catch (Exception ignore) { + // ignore + } + } + } + } + + UNAVAILABILITY_CAUSE = cause; + } + + /** + * Returns {@code true} if and only if the {@code + * netty-transport-native-epoll} is available. + */ + public static boolean isAvailable() { + return UNAVAILABILITY_CAUSE == null; + } + + /** + * Ensure that {@code netty-transport-native-epoll} is + * available. + * + * @throws UnsatisfiedLinkError if unavailable + */ + public static void ensureAvailability() { + if (UNAVAILABILITY_CAUSE != null) { + throw (Error) new UnsatisfiedLinkError( + "failed to load the required native library").initCause(UNAVAILABILITY_CAUSE); + } + } + + /** + * Returns the cause of unavailability of + * {@code netty-transport-native-epoll}. + * + * @return the cause if unavailable. {@code null} if available. + */ + public static Throwable unavailabilityCause() { + return UNAVAILABILITY_CAUSE; + } + + /** + * Returns {@code true} if the epoll native transport is both {@linkplain #isAvailable() available} and supports + * {@linkplain ChannelOption#TCP_FASTOPEN_CONNECT client-side TCP FastOpen}. + * + * @return {@code true} if it's possible to use client-side TCP FastOpen via epoll, otherwise {@code false}. + */ + public static boolean isTcpFastOpenClientSideAvailable() { + return isAvailable() && Native.IS_SUPPORTING_TCP_FASTOPEN_CLIENT; + } + + /** + * Returns {@code true} if the epoll native transport is both {@linkplain #isAvailable() available} and supports + * {@linkplain ChannelOption#TCP_FASTOPEN server-side TCP FastOpen}. + * + * @return {@code true} if it's possible to use server-side TCP FastOpen via epoll, otherwise {@code false}. + */ + public static boolean isTcpFastOpenServerSideAvailable() { + return isAvailable() && Native.IS_SUPPORTING_TCP_FASTOPEN_SERVER; + } + + private Epoll() { + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java new file mode 100644 index 0000000..6f06d74 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java @@ -0,0 +1,237 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelOption; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.unix.IntegerUnixChannelOption; +import io.netty.channel.unix.RawUnixChannelOption; +import io.netty.util.internal.ObjectUtil; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; + +import static io.netty.channel.unix.Limits.SSIZE_MAX; + +public class EpollChannelConfig extends DefaultChannelConfig { + private volatile long maxBytesPerGatheringWrite = SSIZE_MAX; + + protected EpollChannelConfig(Channel channel) { + super(checkAbstractEpollChannel(channel)); + } + + protected EpollChannelConfig(Channel channel, RecvByteBufAllocator recvByteBufAllocator) { + super(checkAbstractEpollChannel(channel), recvByteBufAllocator); + } + + protected LinuxSocket socket() { + return ((AbstractEpollChannel) channel).socket; + } + + private static Channel checkAbstractEpollChannel(Channel channel) { + if (!(channel instanceof AbstractEpollChannel)) { + throw new IllegalArgumentException("channel is not AbstractEpollChannel: " + channel.getClass()); + } + return channel; + } + + @Override + public Map, Object> getOptions() { + return getOptions(super.getOptions(), EpollChannelOption.EPOLL_MODE); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == EpollChannelOption.EPOLL_MODE) { + return (T) getEpollMode(); + } + try { + if (option instanceof IntegerUnixChannelOption) { + IntegerUnixChannelOption opt = (IntegerUnixChannelOption) option; + return (T) Integer.valueOf(((AbstractEpollChannel) channel).socket.getIntOpt( + opt.level(), opt.optname())); + } + if (option instanceof RawUnixChannelOption) { + RawUnixChannelOption opt = (RawUnixChannelOption) option; + ByteBuffer out = ByteBuffer.allocate(opt.length()); + ((AbstractEpollChannel) channel).socket.getRawOpt(opt.level(), opt.optname(), out); + return (T) out.flip(); + } + } catch (IOException e) { + throw new ChannelException(e); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + if (option == EpollChannelOption.EPOLL_MODE) { + setEpollMode((EpollMode) value); + } else { + try { + if (option instanceof IntegerUnixChannelOption) { + IntegerUnixChannelOption opt = (IntegerUnixChannelOption) option; + ((AbstractEpollChannel) channel).socket.setIntOpt(opt.level(), opt.optname(), (Integer) value); + return true; + } else if (option instanceof RawUnixChannelOption) { + RawUnixChannelOption opt = (RawUnixChannelOption) option; + ((AbstractEpollChannel) channel).socket.setRawOpt(opt.level(), opt.optname(), (ByteBuffer) value); + return true; + } + } catch (IOException e) { + throw new ChannelException(e); + } + return super.setOption(option, value); + } + return true; + } + + @Override + public EpollChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + @Deprecated + public EpollChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public EpollChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public EpollChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public EpollChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + if (!(allocator.newHandle() instanceof RecvByteBufAllocator.ExtendedHandle)) { + throw new IllegalArgumentException("allocator.newHandle() must return an object of type: " + + RecvByteBufAllocator.ExtendedHandle.class); + } + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public EpollChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + @Deprecated + public EpollChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + @Deprecated + public EpollChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public EpollChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public EpollChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + /** + * Return the {@link EpollMode} used. Default is + * {@link EpollMode#EDGE_TRIGGERED}. If you want to use {@link #isAutoRead()} {@code false} or + * {@link #getMaxMessagesPerRead()} and have an accurate behaviour you should use + * {@link EpollMode#LEVEL_TRIGGERED}. + */ + public EpollMode getEpollMode() { + return ((AbstractEpollChannel) channel).isFlagSet(Native.EPOLLET) + ? EpollMode.EDGE_TRIGGERED : EpollMode.LEVEL_TRIGGERED; + } + + /** + * Set the {@link EpollMode} used. Default is + * {@link EpollMode#EDGE_TRIGGERED}. If you want to use {@link #isAutoRead()} {@code false} or + * {@link #getMaxMessagesPerRead()} and have an accurate behaviour you should use + * {@link EpollMode#LEVEL_TRIGGERED}. + * + * Be aware this config setting can only be adjusted before the channel was registered. + */ + public EpollChannelConfig setEpollMode(EpollMode mode) { + ObjectUtil.checkNotNull(mode, "mode"); + + try { + switch (mode) { + case EDGE_TRIGGERED: + checkChannelNotRegistered(); + ((AbstractEpollChannel) channel).setFlag(Native.EPOLLET); + break; + case LEVEL_TRIGGERED: + checkChannelNotRegistered(); + ((AbstractEpollChannel) channel).clearFlag(Native.EPOLLET); + break; + default: + throw new Error(); + } + } catch (IOException e) { + throw new ChannelException(e); + } + return this; + } + + private void checkChannelNotRegistered() { + if (channel.isRegistered()) { + throw new IllegalStateException("EpollMode can only be changed before channel is registered"); + } + } + + @Override + protected final void autoReadCleared() { + ((AbstractEpollChannel) channel).clearEpollIn(); + } + + protected final void setMaxBytesPerGatheringWrite(long maxBytesPerGatheringWrite) { + this.maxBytesPerGatheringWrite = maxBytesPerGatheringWrite; + } + + protected final long getMaxBytesPerGatheringWrite() { + return maxBytesPerGatheringWrite; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java new file mode 100644 index 0000000..b10938e --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.channel.ChannelOption; +import io.netty.channel.unix.UnixChannelOption; +import java.net.InetAddress; +import java.util.Map; + +public final class EpollChannelOption extends UnixChannelOption { + + public static final ChannelOption TCP_CORK = valueOf(EpollChannelOption.class, "TCP_CORK"); + public static final ChannelOption TCP_NOTSENT_LOWAT = valueOf(EpollChannelOption.class, "TCP_NOTSENT_LOWAT"); + public static final ChannelOption TCP_KEEPIDLE = valueOf(EpollChannelOption.class, "TCP_KEEPIDLE"); + public static final ChannelOption TCP_KEEPINTVL = valueOf(EpollChannelOption.class, "TCP_KEEPINTVL"); + public static final ChannelOption TCP_KEEPCNT = valueOf(EpollChannelOption.class, "TCP_KEEPCNT"); + public static final ChannelOption TCP_USER_TIMEOUT = + valueOf(EpollChannelOption.class, "TCP_USER_TIMEOUT"); + public static final ChannelOption IP_FREEBIND = valueOf("IP_FREEBIND"); + public static final ChannelOption IP_TRANSPARENT = valueOf("IP_TRANSPARENT"); + public static final ChannelOption IP_RECVORIGDSTADDR = valueOf("IP_RECVORIGDSTADDR"); + /** + * @deprecated Use {@link ChannelOption#TCP_FASTOPEN} instead. + */ + @Deprecated + public static final ChannelOption TCP_FASTOPEN = ChannelOption.TCP_FASTOPEN; + + /** + * @deprecated Use {@link ChannelOption#TCP_FASTOPEN_CONNECT} instead. + */ + @Deprecated + public static final ChannelOption TCP_FASTOPEN_CONNECT = ChannelOption.TCP_FASTOPEN_CONNECT; + public static final ChannelOption TCP_DEFER_ACCEPT = + ChannelOption.valueOf(EpollChannelOption.class, "TCP_DEFER_ACCEPT"); + public static final ChannelOption TCP_QUICKACK = valueOf(EpollChannelOption.class, "TCP_QUICKACK"); + public static final ChannelOption SO_BUSY_POLL = valueOf(EpollChannelOption.class, "SO_BUSY_POLL"); + + public static final ChannelOption EPOLL_MODE = + ChannelOption.valueOf(EpollChannelOption.class, "EPOLL_MODE"); + + public static final ChannelOption> TCP_MD5SIG = valueOf("TCP_MD5SIG"); + + public static final ChannelOption MAX_DATAGRAM_PAYLOAD_SIZE = valueOf("MAX_DATAGRAM_PAYLOAD_SIZE"); + public static final ChannelOption UDP_GRO = valueOf("UDP_GRO"); + + @SuppressWarnings({ "unused", "deprecation" }) + private EpollChannelOption() { + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java new file mode 100644 index 0000000..855912c --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java @@ -0,0 +1,779 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultAddressedEnvelope; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.unix.Errors; +import io.netty.channel.unix.Errors.NativeIoException; +import io.netty.channel.unix.UnixChannelUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.UncheckedBooleanSupplier; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.RecyclableArrayList; +import io.netty.util.internal.StringUtil; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.PortUnreachableException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.UnresolvedAddressException; + +import static io.netty.channel.epoll.LinuxSocket.newSocketDgram; + +/** + * {@link DatagramChannel} implementation that uses linux EPOLL Edge-Triggered Mode for + * maximal performance. + */ +public final class EpollDatagramChannel extends AbstractEpollChannel implements DatagramChannel { + private static final ChannelMetadata METADATA = new ChannelMetadata(true, 16); + private static final String EXPECTED_TYPES = + " (expected: " + StringUtil.simpleClassName(DatagramPacket.class) + ", " + + StringUtil.simpleClassName(AddressedEnvelope.class) + '<' + + StringUtil.simpleClassName(ByteBuf.class) + ", " + + StringUtil.simpleClassName(InetSocketAddress.class) + ">, " + + StringUtil.simpleClassName(ByteBuf.class) + ')'; + + private final EpollDatagramChannelConfig config; + private volatile boolean connected; + + /** + * Returns {@code true} if {@link io.netty.channel.unix.SegmentedDatagramPacket} is supported natively. + * + * @return {@code true} if supported, {@code false} otherwise. + */ + public static boolean isSegmentedDatagramPacketSupported() { + return Epoll.isAvailable() && + // We only support it together with sendmmsg(...) + Native.IS_SUPPORTING_SENDMMSG && Native.IS_SUPPORTING_UDP_SEGMENT; + } + + /** + * Create a new instance which selects the {@link InternetProtocolFamily} to use depending + * on the Operation Systems default which will be chosen. + */ + public EpollDatagramChannel() { + this(null); + } + + /** + * Create a new instance using the given {@link InternetProtocolFamily}. If {@code null} is used it will depend + * on the Operation Systems default which will be chosen. + */ + public EpollDatagramChannel(InternetProtocolFamily family) { + this(newSocketDgram(family), false); + } + + /** + * Create a new instance which selects the {@link InternetProtocolFamily} to use depending + * on the Operation Systems default which will be chosen. + */ + public EpollDatagramChannel(int fd) { + this(new LinuxSocket(fd), true); + } + + private EpollDatagramChannel(LinuxSocket fd, boolean active) { + super(null, fd, active); + config = new EpollDatagramChannelConfig(this); + } + + @Override + public InetSocketAddress remoteAddress() { + return (InetSocketAddress) super.remoteAddress(); + } + + @Override + public InetSocketAddress localAddress() { + return (InetSocketAddress) super.localAddress(); + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + public boolean isActive() { + return socket.isOpen() && (config.getActiveOnOpen() && isRegistered() || active); + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public ChannelFuture joinGroup(InetAddress multicastAddress) { + return joinGroup(multicastAddress, newPromise()); + } + + @Override + public ChannelFuture joinGroup(InetAddress multicastAddress, ChannelPromise promise) { + try { + NetworkInterface iface = config().getNetworkInterface(); + if (iface == null) { + iface = NetworkInterface.getByInetAddress(localAddress().getAddress()); + } + return joinGroup(multicastAddress, iface, null, promise); + } catch (IOException e) { + promise.setFailure(e); + } + return promise; + } + + @Override + public ChannelFuture joinGroup( + InetSocketAddress multicastAddress, NetworkInterface networkInterface) { + return joinGroup(multicastAddress, networkInterface, newPromise()); + } + + @Override + public ChannelFuture joinGroup( + InetSocketAddress multicastAddress, NetworkInterface networkInterface, + ChannelPromise promise) { + return joinGroup(multicastAddress.getAddress(), networkInterface, null, promise); + } + + @Override + public ChannelFuture joinGroup( + InetAddress multicastAddress, NetworkInterface networkInterface, InetAddress source) { + return joinGroup(multicastAddress, networkInterface, source, newPromise()); + } + + @Override + public ChannelFuture joinGroup( + final InetAddress multicastAddress, final NetworkInterface networkInterface, + final InetAddress source, final ChannelPromise promise) { + + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); + + if (eventLoop().inEventLoop()) { + joinGroup0(multicastAddress, networkInterface, source, promise); + } else { + eventLoop().execute(new Runnable() { + @Override + public void run() { + joinGroup0(multicastAddress, networkInterface, source, promise); + } + }); + } + return promise; + } + + private void joinGroup0( + final InetAddress multicastAddress, final NetworkInterface networkInterface, + final InetAddress source, final ChannelPromise promise) { + assert eventLoop().inEventLoop(); + + try { + socket.joinGroup(multicastAddress, networkInterface, source); + promise.setSuccess(); + } catch (IOException e) { + promise.setFailure(e); + } + } + + @Override + public ChannelFuture leaveGroup(InetAddress multicastAddress) { + return leaveGroup(multicastAddress, newPromise()); + } + + @Override + public ChannelFuture leaveGroup(InetAddress multicastAddress, ChannelPromise promise) { + try { + return leaveGroup( + multicastAddress, NetworkInterface.getByInetAddress(localAddress().getAddress()), null, promise); + } catch (IOException e) { + promise.setFailure(e); + } + return promise; + } + + @Override + public ChannelFuture leaveGroup( + InetSocketAddress multicastAddress, NetworkInterface networkInterface) { + return leaveGroup(multicastAddress, networkInterface, newPromise()); + } + + @Override + public ChannelFuture leaveGroup( + InetSocketAddress multicastAddress, + NetworkInterface networkInterface, ChannelPromise promise) { + return leaveGroup(multicastAddress.getAddress(), networkInterface, null, promise); + } + + @Override + public ChannelFuture leaveGroup( + InetAddress multicastAddress, NetworkInterface networkInterface, InetAddress source) { + return leaveGroup(multicastAddress, networkInterface, source, newPromise()); + } + + @Override + public ChannelFuture leaveGroup( + final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source, + final ChannelPromise promise) { + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); + + if (eventLoop().inEventLoop()) { + leaveGroup0(multicastAddress, networkInterface, source, promise); + } else { + eventLoop().execute(new Runnable() { + @Override + public void run() { + leaveGroup0(multicastAddress, networkInterface, source, promise); + } + }); + } + return promise; + } + + private void leaveGroup0( + final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source, + final ChannelPromise promise) { + assert eventLoop().inEventLoop(); + + try { + socket.leaveGroup(multicastAddress, networkInterface, source); + promise.setSuccess(); + } catch (IOException e) { + promise.setFailure(e); + } + } + + @Override + public ChannelFuture block( + InetAddress multicastAddress, NetworkInterface networkInterface, + InetAddress sourceToBlock) { + return block(multicastAddress, networkInterface, sourceToBlock, newPromise()); + } + + @Override + public ChannelFuture block( + final InetAddress multicastAddress, final NetworkInterface networkInterface, + final InetAddress sourceToBlock, final ChannelPromise promise) { + ObjectUtil.checkNotNull(multicastAddress, "multicastAddress"); + ObjectUtil.checkNotNull(sourceToBlock, "sourceToBlock"); + ObjectUtil.checkNotNull(networkInterface, "networkInterface"); + + promise.setFailure(new UnsupportedOperationException("Multicast block not supported")); + return promise; + } + + @Override + public ChannelFuture block(InetAddress multicastAddress, InetAddress sourceToBlock) { + return block(multicastAddress, sourceToBlock, newPromise()); + } + + @Override + public ChannelFuture block( + InetAddress multicastAddress, InetAddress sourceToBlock, ChannelPromise promise) { + try { + return block( + multicastAddress, + NetworkInterface.getByInetAddress(localAddress().getAddress()), + sourceToBlock, promise); + } catch (Throwable e) { + promise.setFailure(e); + } + return promise; + } + + @Override + protected AbstractEpollUnsafe newUnsafe() { + return new EpollDatagramChannelUnsafe(); + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + if (localAddress instanceof InetSocketAddress) { + InetSocketAddress socketAddress = (InetSocketAddress) localAddress; + if (socketAddress.getAddress().isAnyLocalAddress() && + socketAddress.getAddress() instanceof Inet4Address) { + if (socket.family() == InternetProtocolFamily.IPv6) { + localAddress = new InetSocketAddress(LinuxSocket.INET6_ANY, socketAddress.getPort()); + } + } + } + super.doBind(localAddress); + active = true; + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + int maxMessagesPerWrite = maxMessagesPerWrite(); + while (maxMessagesPerWrite > 0) { + Object msg = in.current(); + if (msg == null) { + // Wrote all messages. + break; + } + + try { + // Check if sendmmsg(...) is supported which is only the case for GLIBC 2.14+ + if (Native.IS_SUPPORTING_SENDMMSG && in.size() > 1 || + // We only handle UDP_SEGMENT in sendmmsg. + in.current() instanceof io.netty.channel.unix.SegmentedDatagramPacket) { + NativeDatagramPacketArray array = cleanDatagramPacketArray(); + array.add(in, isConnected(), maxMessagesPerWrite); + int cnt = array.count(); + + if (cnt >= 1) { + // Try to use gathering writes via sendmmsg(...) syscall. + int offset = 0; + NativeDatagramPacketArray.NativeDatagramPacket[] packets = array.packets(); + + int send = socket.sendmmsg(packets, offset, cnt); + if (send == 0) { + // Did not write all messages. + break; + } + for (int i = 0; i < send; i++) { + in.remove(); + } + maxMessagesPerWrite -= send; + continue; + } + } + boolean done = false; + for (int i = config().getWriteSpinCount(); i > 0; --i) { + if (doWriteMessage(msg)) { + done = true; + break; + } + } + + if (done) { + in.remove(); + maxMessagesPerWrite --; + } else { + break; + } + } catch (IOException e) { + maxMessagesPerWrite --; + // Continue on write error as a DatagramChannel can write to multiple remote peers + // + // See https://github.com/netty/netty/issues/2665 + in.remove(e); + } + } + + if (in.isEmpty()) { + // Did write all messages. + clearFlag(Native.EPOLLOUT); + } else { + // Did not write all messages. + setFlag(Native.EPOLLOUT); + } + } + + private boolean doWriteMessage(Object msg) throws Exception { + final ByteBuf data; + final InetSocketAddress remoteAddress; + if (msg instanceof AddressedEnvelope) { + @SuppressWarnings("unchecked") + AddressedEnvelope envelope = + (AddressedEnvelope) msg; + data = envelope.content(); + remoteAddress = envelope.recipient(); + } else { + data = (ByteBuf) msg; + remoteAddress = null; + } + + final int dataLen = data.readableBytes(); + if (dataLen == 0) { + return true; + } + + return doWriteOrSendBytes(data, remoteAddress, false) > 0; + } + + private static void checkUnresolved(AddressedEnvelope envelope) { + if (envelope.recipient() instanceof InetSocketAddress + && (((InetSocketAddress) envelope.recipient()).isUnresolved())) { + throw new UnresolvedAddressException(); + } + } + + @Override + protected Object filterOutboundMessage(Object msg) { + if (msg instanceof io.netty.channel.unix.SegmentedDatagramPacket) { + if (!Native.IS_SUPPORTING_UDP_SEGMENT) { + throw new UnsupportedOperationException( + "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES); + } + io.netty.channel.unix.SegmentedDatagramPacket packet = (io.netty.channel.unix.SegmentedDatagramPacket) msg; + checkUnresolved(packet); + + ByteBuf content = packet.content(); + return UnixChannelUtil.isBufferCopyNeededForWrite(content) ? + packet.replace(newDirectBuffer(packet, content)) : msg; + } + if (msg instanceof DatagramPacket) { + DatagramPacket packet = (DatagramPacket) msg; + checkUnresolved(packet); + + ByteBuf content = packet.content(); + return UnixChannelUtil.isBufferCopyNeededForWrite(content) ? + new DatagramPacket(newDirectBuffer(packet, content), packet.recipient()) : msg; + } + + if (msg instanceof ByteBuf) { + ByteBuf buf = (ByteBuf) msg; + return UnixChannelUtil.isBufferCopyNeededForWrite(buf)? newDirectBuffer(buf) : buf; + } + + if (msg instanceof AddressedEnvelope) { + @SuppressWarnings("unchecked") + AddressedEnvelope e = (AddressedEnvelope) msg; + checkUnresolved(e); + + if (e.content() instanceof ByteBuf && + (e.recipient() == null || e.recipient() instanceof InetSocketAddress)) { + + ByteBuf content = (ByteBuf) e.content(); + return UnixChannelUtil.isBufferCopyNeededForWrite(content)? + new DefaultAddressedEnvelope( + newDirectBuffer(e, content), (InetSocketAddress) e.recipient()) : e; + } + } + + throw new UnsupportedOperationException( + "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES); + } + + @Override + public EpollDatagramChannelConfig config() { + return config; + } + + @Override + protected void doDisconnect() throws Exception { + socket.disconnect(); + connected = active = false; + resetCachedAddresses(); + } + + @Override + protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + if (super.doConnect(remoteAddress, localAddress)) { + connected = true; + return true; + } + return false; + } + + @Override + protected void doClose() throws Exception { + super.doClose(); + connected = false; + } + + final class EpollDatagramChannelUnsafe extends AbstractEpollUnsafe { + + @Override + void epollInReady() { + assert eventLoop().inEventLoop(); + EpollDatagramChannelConfig config = config(); + if (shouldBreakEpollInReady(config)) { + clearEpollIn0(); + return; + } + final EpollRecvByteAllocatorHandle allocHandle = recvBufAllocHandle(); + allocHandle.edgeTriggered(isFlagSet(Native.EPOLLET)); + + final ChannelPipeline pipeline = pipeline(); + final ByteBufAllocator allocator = config.getAllocator(); + allocHandle.reset(config); + epollInBefore(); + + Throwable exception = null; + try { + try { + boolean connected = isConnected(); + do { + final boolean read; + int datagramSize = config().getMaxDatagramPayloadSize(); + + ByteBuf byteBuf = allocHandle.allocate(allocator); + // Only try to use recvmmsg if its really supported by the running system. + int numDatagram = Native.IS_SUPPORTING_RECVMMSG ? + datagramSize == 0 ? 1 : byteBuf.writableBytes() / datagramSize : + 0; + try { + if (numDatagram <= 1) { + if (!connected || config.isUdpGro()) { + read = recvmsg(allocHandle, cleanDatagramPacketArray(), byteBuf); + } else { + read = connectedRead(allocHandle, byteBuf, datagramSize); + } + } else { + // Try to use scattering reads via recvmmsg(...) syscall. + read = scatteringRead(allocHandle, cleanDatagramPacketArray(), + byteBuf, datagramSize, numDatagram); + } + } catch (NativeIoException e) { + if (connected) { + throw translateForConnected(e); + } + throw e; + } + + if (read) { + readPending = false; + } else { + break; + } + // We use the TRUE_SUPPLIER as it is also ok to read less then what we did try to read (as long + // as we read anything). + } while (allocHandle.continueReading(UncheckedBooleanSupplier.TRUE_SUPPLIER)); + } catch (Throwable t) { + exception = t; + } + + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + + if (exception != null) { + pipeline.fireExceptionCaught(exception); + } + } finally { + epollInFinally(config); + } + } + } + + private boolean connectedRead(EpollRecvByteAllocatorHandle allocHandle, ByteBuf byteBuf, int maxDatagramPacketSize) + throws Exception { + try { + int writable = maxDatagramPacketSize != 0 ? Math.min(byteBuf.writableBytes(), maxDatagramPacketSize) + : byteBuf.writableBytes(); + allocHandle.attemptedBytesRead(writable); + + int writerIndex = byteBuf.writerIndex(); + int localReadAmount; + if (byteBuf.hasMemoryAddress()) { + localReadAmount = socket.recvAddress(byteBuf.memoryAddress(), writerIndex, writerIndex + writable); + } else { + ByteBuffer buf = byteBuf.internalNioBuffer(writerIndex, writable); + localReadAmount = socket.recv(buf, buf.position(), buf.limit()); + } + + if (localReadAmount <= 0) { + allocHandle.lastBytesRead(localReadAmount); + + // nothing was read, release the buffer. + return false; + } + byteBuf.writerIndex(writerIndex + localReadAmount); + + allocHandle.lastBytesRead(maxDatagramPacketSize <= 0 ? + localReadAmount : writable); + + DatagramPacket packet = new DatagramPacket(byteBuf, localAddress(), remoteAddress()); + allocHandle.incMessagesRead(1); + + pipeline().fireChannelRead(packet); + byteBuf = null; + return true; + } finally { + if (byteBuf != null) { + byteBuf.release(); + } + } + } + + private IOException translateForConnected(NativeIoException e) { + // We need to correctly translate connect errors to match NIO behaviour. + if (e.expectedErr() == Errors.ERROR_ECONNREFUSED_NEGATIVE) { + PortUnreachableException error = new PortUnreachableException(e.getMessage()); + error.initCause(e); + return error; + } + return e; + } + + private static void addDatagramPacketToOut(DatagramPacket packet, + RecyclableArrayList out) { + if (packet instanceof io.netty.channel.unix.SegmentedDatagramPacket) { + io.netty.channel.unix.SegmentedDatagramPacket segmentedDatagramPacket = + (io.netty.channel.unix.SegmentedDatagramPacket) packet; + ByteBuf content = segmentedDatagramPacket.content(); + InetSocketAddress recipient = segmentedDatagramPacket.recipient(); + InetSocketAddress sender = segmentedDatagramPacket.sender(); + int segmentSize = segmentedDatagramPacket.segmentSize(); + do { + out.add(new DatagramPacket(content.readRetainedSlice(Math.min(content.readableBytes(), + segmentSize)), recipient, sender)); + } while (content.isReadable()); + + segmentedDatagramPacket.release(); + } else { + out.add(packet); + } + } + + private static void releaseAndRecycle(ByteBuf byteBuf, RecyclableArrayList packetList) { + if (byteBuf != null) { + byteBuf.release(); + } + if (packetList != null) { + for (int i = 0; i < packetList.size(); i++) { + ReferenceCountUtil.release(packetList.get(i)); + } + packetList.recycle(); + } + } + + private static void processPacket(ChannelPipeline pipeline, EpollRecvByteAllocatorHandle handle, + int bytesRead, DatagramPacket packet) { + handle.lastBytesRead(Math.max(1, bytesRead)); // Avoid signalling end-of-data for zero-sized datagrams. + handle.incMessagesRead(1); + pipeline.fireChannelRead(packet); + } + + private static void processPacketList(ChannelPipeline pipeline, EpollRecvByteAllocatorHandle handle, + int bytesRead, RecyclableArrayList packetList) { + int messagesRead = packetList.size(); + handle.lastBytesRead(Math.max(1, bytesRead)); // Avoid signalling end-of-data for zero-sized datagrams. + handle.incMessagesRead(messagesRead); + for (int i = 0; i < messagesRead; i++) { + pipeline.fireChannelRead(packetList.set(i, Unpooled.EMPTY_BUFFER)); + } + } + + private boolean recvmsg(EpollRecvByteAllocatorHandle allocHandle, + NativeDatagramPacketArray array, ByteBuf byteBuf) throws IOException { + RecyclableArrayList datagramPackets = null; + try { + int writable = byteBuf.writableBytes(); + + boolean added = array.addWritable(byteBuf, byteBuf.writerIndex(), writable); + assert added; + + allocHandle.attemptedBytesRead(writable); + + NativeDatagramPacketArray.NativeDatagramPacket msg = array.packets()[0]; + + int bytesReceived = socket.recvmsg(msg); + if (!msg.hasSender()) { + allocHandle.lastBytesRead(-1); + return false; + } + byteBuf.writerIndex(bytesReceived); + InetSocketAddress local = localAddress(); + DatagramPacket packet = msg.newDatagramPacket(byteBuf, local); + if (!(packet instanceof io.netty.channel.unix.SegmentedDatagramPacket)) { + processPacket(pipeline(), allocHandle, bytesReceived, packet); + } else { + // Its important that we process all received data out of the NativeDatagramPacketArray + // before we call fireChannelRead(...). This is because the user may call flush() + // in a channelRead(...) method and so may re-use the NativeDatagramPacketArray again. + datagramPackets = RecyclableArrayList.newInstance(); + addDatagramPacketToOut(packet, datagramPackets); + + processPacketList(pipeline(), allocHandle, bytesReceived, datagramPackets); + datagramPackets.recycle(); + datagramPackets = null; + } + + return true; + } finally { + releaseAndRecycle(byteBuf, datagramPackets); + } + } + + private boolean scatteringRead(EpollRecvByteAllocatorHandle allocHandle, NativeDatagramPacketArray array, + ByteBuf byteBuf, int datagramSize, int numDatagram) throws IOException { + RecyclableArrayList datagramPackets = null; + try { + int offset = byteBuf.writerIndex(); + for (int i = 0; i < numDatagram; i++, offset += datagramSize) { + if (!array.addWritable(byteBuf, offset, datagramSize)) { + break; + } + } + + allocHandle.attemptedBytesRead(offset - byteBuf.writerIndex()); + + NativeDatagramPacketArray.NativeDatagramPacket[] packets = array.packets(); + + int received = socket.recvmmsg(packets, 0, array.count()); + if (received == 0) { + allocHandle.lastBytesRead(-1); + return false; + } + + InetSocketAddress local = localAddress(); + + // Set the writerIndex too the maximum number of bytes we might have read. + int bytesReceived = received * datagramSize; + byteBuf.writerIndex(byteBuf.writerIndex() + bytesReceived); + + if (received == 1) { + // Single packet fast-path + DatagramPacket packet = packets[0].newDatagramPacket(byteBuf, local); + if (!(packet instanceof io.netty.channel.unix.SegmentedDatagramPacket)) { + processPacket(pipeline(), allocHandle, datagramSize, packet); + return true; + } + } + // Its important that we process all received data out of the NativeDatagramPacketArray + // before we call fireChannelRead(...). This is because the user may call flush() + // in a channelRead(...) method and so may re-use the NativeDatagramPacketArray again. + datagramPackets = RecyclableArrayList.newInstance(); + for (int i = 0; i < received; i++) { + DatagramPacket packet = packets[i].newDatagramPacket(byteBuf, local); + + // We need to skip the maximum datagram size to ensure we have the readerIndex in the right position + // for the next one. + byteBuf.skipBytes(datagramSize); + addDatagramPacketToOut(packet, datagramPackets); + } + // Ass we did use readRetainedSlice(...) before we should now release the byteBuf and null it out. + byteBuf.release(); + byteBuf = null; + + processPacketList(pipeline(), allocHandle, bytesReceived, datagramPackets); + datagramPackets.recycle(); + datagramPackets = null; + return true; + } finally { + releaseAndRecycle(byteBuf, datagramPackets); + } + } + + private NativeDatagramPacketArray cleanDatagramPacketArray() { + return ((EpollEventLoop) eventLoop()).cleanDatagramPacketArray(); + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java new file mode 100644 index 0000000..4af8358 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java @@ -0,0 +1,566 @@ +/* + * Copyright 2012 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelOption; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.DatagramChannelConfig; +import io.netty.util.internal.ObjectUtil; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Map; + +public final class EpollDatagramChannelConfig extends EpollChannelConfig implements DatagramChannelConfig { + private boolean activeOnOpen; + private volatile int maxDatagramSize; + + EpollDatagramChannelConfig(EpollDatagramChannel channel) { + super(channel, new FixedRecvByteBufAllocator(2048)); + } + + @Override + @SuppressWarnings("deprecation") + public Map, Object> getOptions() { + return getOptions( + super.getOptions(), + ChannelOption.SO_BROADCAST, ChannelOption.SO_RCVBUF, ChannelOption.SO_SNDBUF, + ChannelOption.SO_REUSEADDR, ChannelOption.IP_MULTICAST_LOOP_DISABLED, + ChannelOption.IP_MULTICAST_ADDR, ChannelOption.IP_MULTICAST_IF, ChannelOption.IP_MULTICAST_TTL, + ChannelOption.IP_TOS, ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, + EpollChannelOption.SO_REUSEPORT, EpollChannelOption.IP_FREEBIND, EpollChannelOption.IP_TRANSPARENT, + EpollChannelOption.IP_RECVORIGDSTADDR, EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE, + EpollChannelOption.UDP_GRO); + } + + @SuppressWarnings({ "unchecked", "deprecation" }) + @Override + public T getOption(ChannelOption option) { + if (option == ChannelOption.SO_BROADCAST) { + return (T) Boolean.valueOf(isBroadcast()); + } + if (option == ChannelOption.SO_RCVBUF) { + return (T) Integer.valueOf(getReceiveBufferSize()); + } + if (option == ChannelOption.SO_SNDBUF) { + return (T) Integer.valueOf(getSendBufferSize()); + } + if (option == ChannelOption.SO_REUSEADDR) { + return (T) Boolean.valueOf(isReuseAddress()); + } + if (option == ChannelOption.IP_MULTICAST_LOOP_DISABLED) { + return (T) Boolean.valueOf(isLoopbackModeDisabled()); + } + if (option == ChannelOption.IP_MULTICAST_ADDR) { + return (T) getInterface(); + } + if (option == ChannelOption.IP_MULTICAST_IF) { + return (T) getNetworkInterface(); + } + if (option == ChannelOption.IP_MULTICAST_TTL) { + return (T) Integer.valueOf(getTimeToLive()); + } + if (option == ChannelOption.IP_TOS) { + return (T) Integer.valueOf(getTrafficClass()); + } + if (option == ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION) { + return (T) Boolean.valueOf(activeOnOpen); + } + if (option == EpollChannelOption.SO_REUSEPORT) { + return (T) Boolean.valueOf(isReusePort()); + } + if (option == EpollChannelOption.IP_TRANSPARENT) { + return (T) Boolean.valueOf(isIpTransparent()); + } + if (option == EpollChannelOption.IP_FREEBIND) { + return (T) Boolean.valueOf(isFreeBind()); + } + if (option == EpollChannelOption.IP_RECVORIGDSTADDR) { + return (T) Boolean.valueOf(isIpRecvOrigDestAddr()); + } + if (option == EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE) { + return (T) Integer.valueOf(getMaxDatagramPayloadSize()); + } + if (option == EpollChannelOption.UDP_GRO) { + return (T) Boolean.valueOf(isUdpGro()); + } + return super.getOption(option); + } + + @Override + @SuppressWarnings("deprecation") + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == ChannelOption.SO_BROADCAST) { + setBroadcast((Boolean) value); + } else if (option == ChannelOption.SO_RCVBUF) { + setReceiveBufferSize((Integer) value); + } else if (option == ChannelOption.SO_SNDBUF) { + setSendBufferSize((Integer) value); + } else if (option == ChannelOption.SO_REUSEADDR) { + setReuseAddress((Boolean) value); + } else if (option == ChannelOption.IP_MULTICAST_LOOP_DISABLED) { + setLoopbackModeDisabled((Boolean) value); + } else if (option == ChannelOption.IP_MULTICAST_ADDR) { + setInterface((InetAddress) value); + } else if (option == ChannelOption.IP_MULTICAST_IF) { + setNetworkInterface((NetworkInterface) value); + } else if (option == ChannelOption.IP_MULTICAST_TTL) { + setTimeToLive((Integer) value); + } else if (option == ChannelOption.IP_TOS) { + setTrafficClass((Integer) value); + } else if (option == ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION) { + setActiveOnOpen((Boolean) value); + } else if (option == EpollChannelOption.SO_REUSEPORT) { + setReusePort((Boolean) value); + } else if (option == EpollChannelOption.IP_FREEBIND) { + setFreeBind((Boolean) value); + } else if (option == EpollChannelOption.IP_TRANSPARENT) { + setIpTransparent((Boolean) value); + } else if (option == EpollChannelOption.IP_RECVORIGDSTADDR) { + setIpRecvOrigDestAddr((Boolean) value); + } else if (option == EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE) { + setMaxDatagramPayloadSize((Integer) value); + } else if (option == EpollChannelOption.UDP_GRO) { + setUdpGro((Boolean) value); + } else { + return super.setOption(option, value); + } + + return true; + } + + private void setActiveOnOpen(boolean activeOnOpen) { + if (channel.isRegistered()) { + throw new IllegalStateException("Can only changed before channel was registered"); + } + this.activeOnOpen = activeOnOpen; + } + + boolean getActiveOnOpen() { + return activeOnOpen; + } + + @Override + public EpollDatagramChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + @Override + @Deprecated + public EpollDatagramChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + @Deprecated + public EpollDatagramChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + public EpollDatagramChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public EpollDatagramChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + public EpollDatagramChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public EpollDatagramChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public EpollDatagramChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public EpollDatagramChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public EpollDatagramChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + @Deprecated + public EpollDatagramChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public int getSendBufferSize() { + try { + return ((EpollDatagramChannel) channel).socket.getSendBufferSize(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setSendBufferSize(int sendBufferSize) { + try { + ((EpollDatagramChannel) channel).socket.setSendBufferSize(sendBufferSize); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getReceiveBufferSize() { + try { + return ((EpollDatagramChannel) channel).socket.getReceiveBufferSize(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setReceiveBufferSize(int receiveBufferSize) { + try { + ((EpollDatagramChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getTrafficClass() { + try { + return ((EpollDatagramChannel) channel).socket.getTrafficClass(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setTrafficClass(int trafficClass) { + try { + ((EpollDatagramChannel) channel).socket.setTrafficClass(trafficClass); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public boolean isReuseAddress() { + try { + return ((EpollDatagramChannel) channel).socket.isReuseAddress(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setReuseAddress(boolean reuseAddress) { + try { + ((EpollDatagramChannel) channel).socket.setReuseAddress(reuseAddress); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public boolean isBroadcast() { + try { + return ((EpollDatagramChannel) channel).socket.isBroadcast(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setBroadcast(boolean broadcast) { + try { + ((EpollDatagramChannel) channel).socket.setBroadcast(broadcast); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public boolean isLoopbackModeDisabled() { + try { + return ((EpollDatagramChannel) channel).socket.isLoopbackModeDisabled(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public DatagramChannelConfig setLoopbackModeDisabled(boolean loopbackModeDisabled) { + try { + ((EpollDatagramChannel) channel).socket.setLoopbackModeDisabled(loopbackModeDisabled); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getTimeToLive() { + try { + return ((EpollDatagramChannel) channel).socket.getTimeToLive(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setTimeToLive(int ttl) { + try { + ((EpollDatagramChannel) channel).socket.setTimeToLive(ttl); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public InetAddress getInterface() { + try { + return ((EpollDatagramChannel) channel).socket.getInterface(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setInterface(InetAddress interfaceAddress) { + try { + ((EpollDatagramChannel) channel).socket.setInterface(interfaceAddress); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public NetworkInterface getNetworkInterface() { + try { + return ((EpollDatagramChannel) channel).socket.getNetworkInterface(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setNetworkInterface(NetworkInterface networkInterface) { + try { + EpollDatagramChannel datagramChannel = (EpollDatagramChannel) channel; + datagramChannel.socket.setNetworkInterface(networkInterface); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDatagramChannelConfig setEpollMode(EpollMode mode) { + super.setEpollMode(mode); + return this; + } + + /** + * Returns {@code true} if the SO_REUSEPORT option is set. + */ + public boolean isReusePort() { + try { + return ((EpollDatagramChannel) channel).socket.isReusePort(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the SO_REUSEPORT option on the underlying Channel. This will allow to bind multiple + * {@link EpollSocketChannel}s to the same port and so accept connections with multiple threads. + * + * Be aware this method needs be called before {@link EpollDatagramChannel#bind(java.net.SocketAddress)} to have + * any affect. + */ + public EpollDatagramChannelConfig setReusePort(boolean reusePort) { + try { + ((EpollDatagramChannel) channel).socket.setReusePort(reusePort); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if IP_TRANSPARENT is enabled, + * {@code false} otherwise. + */ + public boolean isIpTransparent() { + try { + return ((EpollDatagramChannel) channel).socket.isIpTransparent(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * If {@code true} is used IP_TRANSPARENT is enabled, + * {@code false} for disable it. Default is disabled. + */ + public EpollDatagramChannelConfig setIpTransparent(boolean ipTransparent) { + try { + ((EpollDatagramChannel) channel).socket.setIpTransparent(ipTransparent); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if IP_FREEBIND is enabled, + * {@code false} otherwise. + */ + public boolean isFreeBind() { + try { + return ((EpollDatagramChannel) channel).socket.isIpFreeBind(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * If {@code true} is used IP_FREEBIND is enabled, + * {@code false} for disable it. Default is disabled. + */ + public EpollDatagramChannelConfig setFreeBind(boolean freeBind) { + try { + ((EpollDatagramChannel) channel).socket.setIpFreeBind(freeBind); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if IP_RECVORIGDSTADDR is + * enabled, {@code false} otherwise. + */ + public boolean isIpRecvOrigDestAddr() { + try { + return ((EpollDatagramChannel) channel).socket.isIpRecvOrigDestAddr(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * If {@code true} is used IP_RECVORIGDSTADDR is + * enabled, {@code false} for disable it. Default is disabled. + */ + public EpollDatagramChannelConfig setIpRecvOrigDestAddr(boolean ipTransparent) { + try { + ((EpollDatagramChannel) channel).socket.setIpRecvOrigDestAddr(ipTransparent); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the maximum {@link io.netty.channel.socket.DatagramPacket} size. This will be used to determine if + * {@code recvmmsg} should be used when reading from the underlying socket. When {@code recvmmsg} is used + * we may be able to read multiple {@link io.netty.channel.socket.DatagramPacket}s with one syscall and so + * greatly improve the performance. This number will be used to slice {@link ByteBuf}s returned by the used + * {@link RecvByteBufAllocator}. You can use {@code 0} to disable the usage of recvmmsg, any other bigger value + * will enable it. + */ + public EpollDatagramChannelConfig setMaxDatagramPayloadSize(int maxDatagramSize) { + this.maxDatagramSize = ObjectUtil.checkPositiveOrZero(maxDatagramSize, "maxDatagramSize"); + return this; + } + + /** + * Get the maximum {@link io.netty.channel.socket.DatagramPacket} size. + */ + public int getMaxDatagramPayloadSize() { + return maxDatagramSize; + } + + private volatile boolean gro; + + /** + * Enable / disable UDP_GRO. + * @param gro {@code true} if {@code UDP_GRO} should be enabled, {@code false} otherwise. + * @return this. + */ + public EpollDatagramChannelConfig setUdpGro(boolean gro) { + try { + ((EpollDatagramChannel) channel).socket.setUdpGro(gro); + } catch (IOException e) { + throw new ChannelException(e); + } + this.gro = gro; + return this; + } + + /** + * Returns if {@code UDP_GRO} is enabled. + * @return {@code true} if enabled, {@code false} otherwise. + */ + public boolean isUdpGro() { + // We don't do a syscall here but just return the cached value due a kernel bug: + // https://lore.kernel.org/netdev/20210325195614.800687-1-norman_maurer@apple.com/T/#u + return gro; + } + + @Override + public EpollDatagramChannelConfig setMaxMessagesPerWrite(int maxMessagesPerWrite) { + super.setMaxMessagesPerWrite(maxMessagesPerWrite); + return this; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainDatagramChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainDatagramChannel.java new file mode 100644 index 0000000..a14d5e4 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainDatagramChannel.java @@ -0,0 +1,382 @@ +/* + * 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.DefaultAddressedEnvelope; +import io.netty.channel.unix.DomainDatagramChannel; +import io.netty.channel.unix.DomainDatagramChannelConfig; +import io.netty.channel.unix.DomainDatagramPacket; +import io.netty.channel.unix.DomainDatagramSocketAddress; +import io.netty.channel.unix.DomainSocketAddress; +import io.netty.channel.unix.IovArray; +import io.netty.channel.unix.PeerCredentials; +import io.netty.channel.unix.UnixChannelUtil; +import io.netty.util.CharsetUtil; +import io.netty.util.UncheckedBooleanSupplier; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; + +import static io.netty.channel.epoll.LinuxSocket.newSocketDomainDgram; + +@UnstableApi +public final class EpollDomainDatagramChannel extends AbstractEpollChannel implements DomainDatagramChannel { + + private static final ChannelMetadata METADATA = new ChannelMetadata(true, 16); + + private static final String EXPECTED_TYPES = + " (expected: " + + StringUtil.simpleClassName(DomainDatagramPacket.class) + ", " + + StringUtil.simpleClassName(AddressedEnvelope.class) + '<' + + StringUtil.simpleClassName(ByteBuf.class) + ", " + + StringUtil.simpleClassName(DomainSocketAddress.class) + ">, " + + StringUtil.simpleClassName(ByteBuf.class) + ')'; + + private volatile boolean connected; + private volatile DomainSocketAddress local; + private volatile DomainSocketAddress remote; + + private final EpollDomainDatagramChannelConfig config; + + public EpollDomainDatagramChannel() { + this(newSocketDomainDgram(), false); + } + + public EpollDomainDatagramChannel(int fd) { + this(new LinuxSocket(fd), true); + } + + private EpollDomainDatagramChannel(LinuxSocket socket, boolean active) { + super(null, socket, active); + config = new EpollDomainDatagramChannelConfig(this); + } + + @Override + public EpollDomainDatagramChannelConfig config() { + return config; + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + super.doBind(localAddress); + local = (DomainSocketAddress) localAddress; + active = true; + } + + @Override + protected void doClose() throws Exception { + super.doClose(); + connected = active = false; + local = null; + remote = null; + } + + @Override + protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + if (super.doConnect(remoteAddress, localAddress)) { + if (localAddress != null) { + local = (DomainSocketAddress) localAddress; + } + remote = (DomainSocketAddress) remoteAddress; + connected = true; + return true; + } + return false; + } + + @Override + protected void doDisconnect() throws Exception { + doClose(); + } + + @Override + protected void doWrite(ChannelOutboundBuffer in) throws Exception { + int maxMessagesPerWrite = maxMessagesPerWrite(); + while (maxMessagesPerWrite > 0) { + Object msg = in.current(); + if (msg == null) { + break; + } + + try { + boolean done = false; + for (int i = config().getWriteSpinCount(); i > 0; --i) { + if (doWriteMessage(msg)) { + done = true; + break; + } + } + + if (done) { + in.remove(); + maxMessagesPerWrite--; + } else { + break; + } + } catch (IOException e) { + maxMessagesPerWrite--; + + // Continue on write error as a DatagramChannel can write to multiple remote peers + // + // See https://github.com/netty/netty/issues/2665 + in.remove(e); + } + } + + if (in.isEmpty()) { + // Did write all messages. + clearFlag(Native.EPOLLOUT); + } else { + // Did not write all messages. + setFlag(Native.EPOLLOUT); + } + } + + private boolean doWriteMessage(Object msg) throws Exception { + final ByteBuf data; + DomainSocketAddress remoteAddress; + if (msg instanceof AddressedEnvelope) { + @SuppressWarnings("unchecked") + AddressedEnvelope envelope = + (AddressedEnvelope) msg; + data = envelope.content(); + remoteAddress = envelope.recipient(); + } else { + data = (ByteBuf) msg; + remoteAddress = null; + } + + final int dataLen = data.readableBytes(); + if (dataLen == 0) { + return true; + } + + final long writtenBytes; + if (data.hasMemoryAddress()) { + long memoryAddress = data.memoryAddress(); + if (remoteAddress == null) { + writtenBytes = socket.sendAddress(memoryAddress, data.readerIndex(), data.writerIndex()); + } else { + writtenBytes = socket.sendToAddressDomainSocket(memoryAddress, data.readerIndex(), data.writerIndex(), + remoteAddress.path().getBytes(CharsetUtil.UTF_8)); + } + } else if (data.nioBufferCount() > 1) { + IovArray array = ((EpollEventLoop) eventLoop()).cleanIovArray(); + array.add(data, data.readerIndex(), data.readableBytes()); + int cnt = array.count(); + assert cnt != 0; + + if (remoteAddress == null) { + writtenBytes = socket.writevAddresses(array.memoryAddress(0), cnt); + } else { + writtenBytes = socket.sendToAddressesDomainSocket(array.memoryAddress(0), cnt, + remoteAddress.path().getBytes(CharsetUtil.UTF_8)); + } + } else { + ByteBuffer nioData = data.internalNioBuffer(data.readerIndex(), data.readableBytes()); + if (remoteAddress == null) { + writtenBytes = socket.send(nioData, nioData.position(), nioData.limit()); + } else { + writtenBytes = socket.sendToDomainSocket(nioData, nioData.position(), nioData.limit(), + remoteAddress.path().getBytes(CharsetUtil.UTF_8)); + } + } + + return writtenBytes > 0; + } + + @Override + protected Object filterOutboundMessage(Object msg) { + if (msg instanceof DomainDatagramPacket) { + DomainDatagramPacket packet = (DomainDatagramPacket) msg; + ByteBuf content = packet.content(); + return UnixChannelUtil.isBufferCopyNeededForWrite(content) ? + new DomainDatagramPacket(newDirectBuffer(packet, content), packet.recipient()) : msg; + } + + if (msg instanceof ByteBuf) { + ByteBuf buf = (ByteBuf) msg; + return UnixChannelUtil.isBufferCopyNeededForWrite(buf) ? newDirectBuffer(buf) : buf; + } + + if (msg instanceof AddressedEnvelope) { + @SuppressWarnings("unchecked") + AddressedEnvelope e = (AddressedEnvelope) msg; + if (e.content() instanceof ByteBuf && + (e.recipient() == null || e.recipient() instanceof DomainSocketAddress)) { + + ByteBuf content = (ByteBuf) e.content(); + return UnixChannelUtil.isBufferCopyNeededForWrite(content) ? + new DefaultAddressedEnvelope( + newDirectBuffer(e, content), (DomainSocketAddress) e.recipient()) : e; + } + } + + throw new UnsupportedOperationException( + "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES); + } + + @Override + public boolean isActive() { + return socket.isOpen() && (config.getActiveOnOpen() && isRegistered() || active); + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public DomainSocketAddress localAddress() { + return (DomainSocketAddress) super.localAddress(); + } + + @Override + protected DomainSocketAddress localAddress0() { + return local; + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + protected AbstractEpollUnsafe newUnsafe() { + return new EpollDomainDatagramChannelUnsafe(); + } + + /** + * Returns the unix credentials (uid, gid, pid) of the peer + * SO_PEERCRED + */ + public PeerCredentials peerCredentials() throws IOException { + return socket.getPeerCredentials(); + } + + @Override + public DomainSocketAddress remoteAddress() { + return (DomainSocketAddress) super.remoteAddress(); + } + + @Override + protected DomainSocketAddress remoteAddress0() { + return remote; + } + + final class EpollDomainDatagramChannelUnsafe extends AbstractEpollUnsafe { + + @Override + void epollInReady() { + assert eventLoop().inEventLoop(); + final DomainDatagramChannelConfig config = config(); + if (shouldBreakEpollInReady(config)) { + clearEpollIn0(); + return; + } + final EpollRecvByteAllocatorHandle allocHandle = recvBufAllocHandle(); + allocHandle.edgeTriggered(isFlagSet(Native.EPOLLET)); + + final ChannelPipeline pipeline = pipeline(); + final ByteBufAllocator allocator = config.getAllocator(); + allocHandle.reset(config); + epollInBefore(); + + Throwable exception = null; + try { + ByteBuf byteBuf = null; + try { + boolean connected = isConnected(); + do { + byteBuf = allocHandle.allocate(allocator); + allocHandle.attemptedBytesRead(byteBuf.writableBytes()); + + final DomainDatagramPacket packet; + if (connected) { + allocHandle.lastBytesRead(doReadBytes(byteBuf)); + if (allocHandle.lastBytesRead() <= 0) { + // nothing was read, release the buffer. + byteBuf.release(); + break; + } + packet = new DomainDatagramPacket(byteBuf, (DomainSocketAddress) localAddress(), + (DomainSocketAddress) remoteAddress()); + } else { + final DomainDatagramSocketAddress remoteAddress; + if (byteBuf.hasMemoryAddress()) { + // has a memory address so use optimized call + remoteAddress = socket.recvFromAddressDomainSocket(byteBuf.memoryAddress(), + byteBuf.writerIndex(), byteBuf.capacity()); + } else { + ByteBuffer nioData = byteBuf.internalNioBuffer( + byteBuf.writerIndex(), byteBuf.writableBytes()); + remoteAddress = + socket.recvFromDomainSocket(nioData, nioData.position(), nioData.limit()); + } + + if (remoteAddress == null) { + allocHandle.lastBytesRead(-1); + byteBuf.release(); + break; + } + DomainSocketAddress localAddress = remoteAddress.localAddress(); + if (localAddress == null) { + localAddress = (DomainSocketAddress) localAddress(); + } + allocHandle.lastBytesRead(remoteAddress.receivedAmount()); + byteBuf.writerIndex(byteBuf.writerIndex() + allocHandle.lastBytesRead()); + + packet = new DomainDatagramPacket(byteBuf, localAddress, remoteAddress); + } + + allocHandle.incMessagesRead(1); + + readPending = false; + pipeline.fireChannelRead(packet); + + byteBuf = null; + + // We use the TRUE_SUPPLIER as it is also ok to read less then what we did try to read (as long + // as we read anything). + } while (allocHandle.continueReading(UncheckedBooleanSupplier.TRUE_SUPPLIER)); + } catch (Throwable t) { + if (byteBuf != null) { + byteBuf.release(); + } + exception = t; + } + + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + + if (exception != null) { + pipeline.fireExceptionCaught(exception); + } + } finally { + epollInFinally(config); + } + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainDatagramChannelConfig.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainDatagramChannelConfig.java new file mode 100644 index 0000000..0138095 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainDatagramChannelConfig.java @@ -0,0 +1,175 @@ +/* + * 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.channel.epoll; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelOption; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.unix.DomainDatagramChannelConfig; +import io.netty.util.internal.UnstableApi; + +import java.io.IOException; +import java.util.Map; + +import static io.netty.channel.ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION; +import static io.netty.channel.ChannelOption.SO_SNDBUF; + +@UnstableApi +public final class EpollDomainDatagramChannelConfig extends EpollChannelConfig implements DomainDatagramChannelConfig { + + private boolean activeOnOpen; + + EpollDomainDatagramChannelConfig(EpollDomainDatagramChannel channel) { + super(channel, new FixedRecvByteBufAllocator(2048)); + } + + @Override + @SuppressWarnings("deprecation") + public Map, Object> getOptions() { + return getOptions( + super.getOptions(), + DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, SO_SNDBUF); + } + + @Override + @SuppressWarnings({"unchecked", "deprecation"}) + public T getOption(ChannelOption option) { + if (option == DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION) { + return (T) Boolean.valueOf(activeOnOpen); + } + if (option == SO_SNDBUF) { + return (T) Integer.valueOf(getSendBufferSize()); + } + return super.getOption(option); + } + + @Override + @SuppressWarnings("deprecation") + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION) { + setActiveOnOpen((Boolean) value); + } else if (option == SO_SNDBUF) { + setSendBufferSize((Integer) value); + } else { + return super.setOption(option, value); + } + + return true; + } + + private void setActiveOnOpen(boolean activeOnOpen) { + if (channel.isRegistered()) { + throw new IllegalStateException("Can only changed before channel was registered"); + } + this.activeOnOpen = activeOnOpen; + } + + boolean getActiveOnOpen() { + return activeOnOpen; + } + + @Override + public EpollDomainDatagramChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setEpollMode(EpollMode mode) { + super.setEpollMode(mode); + return this; + } + + @Override + @Deprecated + public EpollDomainDatagramChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setMaxMessagesPerWrite(int maxMessagesPerWrite) { + super.setMaxMessagesPerWrite(maxMessagesPerWrite); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setSendBufferSize(int sendBufferSize) { + try { + ((EpollDomainDatagramChannel) channel).socket.setSendBufferSize(sendBufferSize); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getSendBufferSize() { + try { + return ((EpollDomainDatagramChannel) channel).socket.getSendBufferSize(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollDomainDatagramChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public EpollDomainDatagramChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannel.java new file mode 100644 index 0000000..b72dd69 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannel.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.unix.DomainSocketAddress; +import io.netty.channel.unix.DomainSocketChannel; +import io.netty.channel.unix.FileDescriptor; +import io.netty.channel.unix.PeerCredentials; +import io.netty.util.internal.UnstableApi; + +import java.io.IOException; +import java.net.SocketAddress; + +import static io.netty.channel.epoll.LinuxSocket.newSocketDomain; + +public final class EpollDomainSocketChannel extends AbstractEpollStreamChannel implements DomainSocketChannel { + private final EpollDomainSocketChannelConfig config = new EpollDomainSocketChannelConfig(this); + + private volatile DomainSocketAddress local; + private volatile DomainSocketAddress remote; + + public EpollDomainSocketChannel() { + super(newSocketDomain(), false); + } + + EpollDomainSocketChannel(Channel parent, FileDescriptor fd) { + this(parent, new LinuxSocket(fd.intValue())); + } + + public EpollDomainSocketChannel(int fd) { + super(fd); + } + + public EpollDomainSocketChannel(Channel parent, LinuxSocket fd) { + super(parent, fd); + local = fd.localDomainSocketAddress(); + remote = fd.remoteDomainSocketAddress(); + } + + public EpollDomainSocketChannel(int fd, boolean active) { + super(new LinuxSocket(fd), active); + } + + @Override + protected AbstractEpollUnsafe newUnsafe() { + return new EpollDomainUnsafe(); + } + + @Override + protected DomainSocketAddress localAddress0() { + return local; + } + + @Override + protected DomainSocketAddress remoteAddress0() { + return remote; + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + socket.bind(localAddress); + local = (DomainSocketAddress) localAddress; + } + + @Override + public EpollDomainSocketChannelConfig config() { + return config; + } + + @Override + protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception { + if (super.doConnect(remoteAddress, localAddress)) { + local = localAddress != null ? (DomainSocketAddress) localAddress : socket.localDomainSocketAddress(); + remote = (DomainSocketAddress) remoteAddress; + return true; + } + return false; + } + + @Override + public DomainSocketAddress remoteAddress() { + return (DomainSocketAddress) super.remoteAddress(); + } + + @Override + public DomainSocketAddress localAddress() { + return (DomainSocketAddress) super.localAddress(); + } + + @Override + protected int doWriteSingle(ChannelOutboundBuffer in) throws Exception { + Object msg = in.current(); + if (msg instanceof FileDescriptor && socket.sendFd(((FileDescriptor) msg).intValue()) > 0) { + // File descriptor was written, so remove it. + in.remove(); + return 1; + } + return super.doWriteSingle(in); + } + + @Override + protected Object filterOutboundMessage(Object msg) { + if (msg instanceof FileDescriptor) { + return msg; + } + return super.filterOutboundMessage(msg); + } + + /** + * Returns the unix credentials (uid, gid, pid) of the peer + * SO_PEERCRED + */ + @UnstableApi + public PeerCredentials peerCredentials() throws IOException { + return socket.getPeerCredentials(); + } + + private final class EpollDomainUnsafe extends EpollStreamUnsafe { + @Override + void epollInReady() { + switch (config().getReadMode()) { + case BYTES: + super.epollInReady(); + break; + case FILE_DESCRIPTORS: + epollInReadFd(); + break; + default: + throw new Error(); + } + } + + private void epollInReadFd() { + if (socket.isInputShutdown()) { + clearEpollIn0(); + return; + } + final ChannelConfig config = config(); + final EpollRecvByteAllocatorHandle allocHandle = recvBufAllocHandle(); + allocHandle.edgeTriggered(isFlagSet(Native.EPOLLET)); + + final ChannelPipeline pipeline = pipeline(); + allocHandle.reset(config); + epollInBefore(); + + try { + readLoop: do { + // lastBytesRead represents the fd. We use lastBytesRead because it must be set so that the + // EpollRecvByteAllocatorHandle knows if it should try to read again or not when autoRead is + // enabled. + allocHandle.lastBytesRead(socket.recvFd()); + switch(allocHandle.lastBytesRead()) { + case 0: + break readLoop; + case -1: + close(voidPromise()); + return; + default: + allocHandle.incMessagesRead(1); + readPending = false; + pipeline.fireChannelRead(new FileDescriptor(allocHandle.lastBytesRead())); + break; + } + } while (allocHandle.continueReading()); + + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + } catch (Throwable t) { + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + pipeline.fireExceptionCaught(t); + } finally { + epollInFinally(config); + } + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java new file mode 100644 index 0000000..407e2ca --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java @@ -0,0 +1,217 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelOption; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.DuplexChannelConfig; +import io.netty.channel.unix.DomainSocketChannelConfig; +import io.netty.channel.unix.DomainSocketReadMode; +import io.netty.util.internal.ObjectUtil; + +import java.io.IOException; +import java.util.Map; + +import static io.netty.channel.ChannelOption.ALLOW_HALF_CLOSURE; +import static io.netty.channel.ChannelOption.SO_RCVBUF; +import static io.netty.channel.ChannelOption.SO_SNDBUF; +import static io.netty.channel.unix.UnixChannelOption.DOMAIN_SOCKET_READ_MODE; + +public final class EpollDomainSocketChannelConfig extends EpollChannelConfig + implements DomainSocketChannelConfig, DuplexChannelConfig { + private volatile DomainSocketReadMode mode = DomainSocketReadMode.BYTES; + private volatile boolean allowHalfClosure; + + EpollDomainSocketChannelConfig(AbstractEpollChannel channel) { + super(channel); + } + + @Override + public Map, Object> getOptions() { + return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE, ALLOW_HALF_CLOSURE, SO_SNDBUF, SO_RCVBUF); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == DOMAIN_SOCKET_READ_MODE) { + return (T) getReadMode(); + } + if (option == ALLOW_HALF_CLOSURE) { + return (T) Boolean.valueOf(isAllowHalfClosure()); + } + if (option == SO_SNDBUF) { + return (T) Integer.valueOf(getSendBufferSize()); + } + if (option == SO_RCVBUF) { + return (T) Integer.valueOf(getReceiveBufferSize()); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == DOMAIN_SOCKET_READ_MODE) { + setReadMode((DomainSocketReadMode) value); + } else if (option == ALLOW_HALF_CLOSURE) { + setAllowHalfClosure((Boolean) value); + } else if (option == SO_SNDBUF) { + setSendBufferSize((Integer) value); + } else if (option == SO_RCVBUF) { + setReceiveBufferSize((Integer) value); + } else { + return super.setOption(option, value); + } + + return true; + } + + @Override + @Deprecated + public EpollDomainSocketChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + @Override + @Deprecated + public EpollDomainSocketChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + @Deprecated + public EpollDomainSocketChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setEpollMode(EpollMode mode) { + super.setEpollMode(mode); + return this; + } + + @Override + public EpollDomainSocketChannelConfig setReadMode(DomainSocketReadMode mode) { + this.mode = ObjectUtil.checkNotNull(mode, "mode"); + return this; + } + + @Override + public DomainSocketReadMode getReadMode() { + return mode; + } + + @Override + public boolean isAllowHalfClosure() { + return allowHalfClosure; + } + + @Override + public EpollDomainSocketChannelConfig setAllowHalfClosure(boolean allowHalfClosure) { + this.allowHalfClosure = allowHalfClosure; + return this; + } + + public int getSendBufferSize() { + try { + return ((EpollDomainSocketChannel) channel).socket.getSendBufferSize(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public EpollDomainSocketChannelConfig setSendBufferSize(int sendBufferSize) { + try { + ((EpollDomainSocketChannel) channel).socket.setSendBufferSize(sendBufferSize); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public int getReceiveBufferSize() { + try { + return ((EpollDomainSocketChannel) channel).socket.getReceiveBufferSize(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public EpollDomainSocketChannelConfig setReceiveBufferSize(int receiveBufferSize) { + try { + ((EpollDomainSocketChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventArray.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventArray.java new file mode 100644 index 0000000..a19cdb7 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventArray.java @@ -0,0 +1,123 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.channel.unix.Buffer; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.UnstableApi; + +import java.nio.ByteBuffer; + +/** + * This is an internal datastructure which can be directly passed to epoll_wait to reduce the overhead. + * + * typedef union epoll_data { + * void *ptr; + * int fd; + * uint32_t u32; + * uint64_t u64; + * } epoll_data_t; + * + * struct epoll_event { + * uint32_t events; // Epoll events + * epoll_data_t data; // User data variable + * }; + * + * We use {@code fd} if the {@code epoll_data union} to store the actual file descriptor of an + * {@link AbstractEpollChannel} and so be able to map it later. + */ +@UnstableApi +public final class EpollEventArray { + // Size of the epoll_event struct + private static final int EPOLL_EVENT_SIZE = Native.sizeofEpollEvent(); + // The offset of the data union in the epoll_event struct + private static final int EPOLL_DATA_OFFSET = Native.offsetofEpollData(); + + private ByteBuffer memory; + private long memoryAddress; + private int length; + + EpollEventArray(int length) { + if (length < 1) { + throw new IllegalArgumentException("length must be >= 1 but was " + length); + } + this.length = length; + memory = Buffer.allocateDirectWithNativeOrder(calculateBufferCapacity(length)); + memoryAddress = Buffer.memoryAddress(memory); + } + + /** + * Return the {@code memoryAddress} which points to the start of this {@link EpollEventArray}. + */ + long memoryAddress() { + return memoryAddress; + } + + /** + * Return the length of the {@link EpollEventArray} which represent the maximum number of {@code epoll_events} + * that can be stored in it. + */ + int length() { + return length; + } + + /** + * Increase the storage of this {@link EpollEventArray}. + */ + void increase() { + // double the size + length <<= 1; + // There is no need to preserve what was in the memory before. + ByteBuffer buffer = Buffer.allocateDirectWithNativeOrder(calculateBufferCapacity(length)); + Buffer.free(memory); + memory = buffer; + memoryAddress = Buffer.memoryAddress(buffer); + } + + /** + * Free this {@link EpollEventArray}. Any usage after calling this method may segfault the JVM! + */ + void free() { + Buffer.free(memory); + memoryAddress = 0; + } + + /** + * Return the events for the {@code epoll_event} on this index. + */ + int events(int index) { + return getInt(index, 0); + } + + /** + * Return the file descriptor for the {@code epoll_event} on this index. + */ + int fd(int index) { + return getInt(index, EPOLL_DATA_OFFSET); + } + + private int getInt(int index, int offset) { + if (PlatformDependent.hasUnsafe()) { + long n = (long) index * EPOLL_EVENT_SIZE; + return PlatformDependent.getInt(memoryAddress + n + offset); + } + return memory.getInt(index * EPOLL_EVENT_SIZE + offset); + } + + private static int calculateBufferCapacity(int capacity) { + return capacity * EPOLL_EVENT_SIZE; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java new file mode 100644 index 0000000..a4a6317 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java @@ -0,0 +1,595 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.EventLoopTaskQueueFactory; +import io.netty.channel.SelectStrategy; +import io.netty.channel.SingleThreadEventLoop; +import io.netty.channel.epoll.AbstractEpollChannel.AbstractEpollUnsafe; +import io.netty.channel.unix.FileDescriptor; +import io.netty.channel.unix.IovArray; +import io.netty.util.IntSupplier; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; +import io.netty.util.concurrent.RejectedExecutionHandler; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.UnstableApi; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; + +import static java.lang.Math.min; + +/** + * {@link EventLoop} which uses epoll under the covers. Only works on Linux! + */ +public class EpollEventLoop extends SingleThreadEventLoop { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(EpollEventLoop.class); + private static final long EPOLL_WAIT_MILLIS_THRESHOLD = + SystemPropertyUtil.getLong("io.netty.channel.epoll.epollWaitThreshold", 10); + + static { + // Ensure JNI is initialized by the time this class is loaded by this time! + // We use unix-common methods in this class which are backed by JNI methods. + Epoll.ensureAvailability(); + } + + private FileDescriptor epollFd; + private FileDescriptor eventFd; + private FileDescriptor timerFd; + private final IntObjectMap channels = new IntObjectHashMap(4096); + private final boolean allowGrowing; + private final EpollEventArray events; + + // These are initialized on first use + private IovArray iovArray; + private NativeDatagramPacketArray datagramPacketArray; + + private final SelectStrategy selectStrategy; + private final IntSupplier selectNowSupplier = new IntSupplier() { + @Override + public int get() throws Exception { + return epollWaitNow(); + } + }; + + private static final long AWAKE = -1L; + private static final long NONE = Long.MAX_VALUE; + + // nextWakeupNanos is: + // AWAKE when EL is awake + // NONE when EL is waiting with no wakeup scheduled + // other value T when EL is waiting with wakeup scheduled at time T + private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE); + private boolean pendingWakeup; + private volatile int ioRatio = 50; + + // See https://man7.org/linux/man-pages/man2/timerfd_create.2.html. + private static final long MAX_SCHEDULED_TIMERFD_NS = 999999999; + + EpollEventLoop(EventLoopGroup parent, Executor executor, int maxEvents, + SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) { + super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory), + rejectedExecutionHandler); + selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy"); + if (maxEvents == 0) { + allowGrowing = true; + events = new EpollEventArray(4096); + } else { + allowGrowing = false; + events = new EpollEventArray(maxEvents); + } + openFileDescriptors(); + } + + /** + * This method is intended for use by a process checkpoint/restore + * integration, such as OpenJDK CRaC. + */ + @UnstableApi + public void openFileDescriptors() { + boolean success = false; + FileDescriptor epollFd = null; + FileDescriptor eventFd = null; + FileDescriptor timerFd = null; + try { + this.epollFd = epollFd = Native.newEpollCreate(); + this.eventFd = eventFd = Native.newEventFd(); + try { + // It is important to use EPOLLET here as we only want to get the notification once per + // wakeup and don't call eventfd_read(...). + Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET); + } catch (IOException e) { + throw new IllegalStateException("Unable to add eventFd filedescriptor to epoll", e); + } + this.timerFd = timerFd = Native.newTimerFd(); + try { + // It is important to use EPOLLET here as we only want to get the notification once per + // wakeup and don't call read(...). + Native.epollCtlAdd(epollFd.intValue(), timerFd.intValue(), Native.EPOLLIN | Native.EPOLLET); + } catch (IOException e) { + throw new IllegalStateException("Unable to add timerFd filedescriptor to epoll", e); + } + success = true; + } finally { + if (!success) { + if (epollFd != null) { + try { + epollFd.close(); + } catch (Exception e) { + // ignore + } + } + if (eventFd != null) { + try { + eventFd.close(); + } catch (Exception e) { + // ignore + } + } + if (timerFd != null) { + try { + timerFd.close(); + } catch (Exception e) { + // ignore + } + } + } + } + } + + private static Queue newTaskQueue( + EventLoopTaskQueueFactory queueFactory) { + if (queueFactory == null) { + return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS); + } + return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS); + } + + /** + * Return a cleared {@link IovArray} that can be used for writes in this {@link EventLoop}. + */ + IovArray cleanIovArray() { + if (iovArray == null) { + iovArray = new IovArray(); + } else { + iovArray.clear(); + } + return iovArray; + } + + /** + * Return a cleared {@link NativeDatagramPacketArray} that can be used for writes in this {@link EventLoop}. + */ + NativeDatagramPacketArray cleanDatagramPacketArray() { + if (datagramPacketArray == null) { + datagramPacketArray = new NativeDatagramPacketArray(); + } else { + datagramPacketArray.clear(); + } + return datagramPacketArray; + } + + @Override + protected void wakeup(boolean inEventLoop) { + if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) { + // write to the evfd which will then wake-up epoll_wait(...) + Native.eventFdWrite(eventFd.intValue(), 1L); + } + } + + @Override + protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + + @Override + protected boolean afterScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + + /** + * Register the given epoll with this {@link EventLoop}. + */ + void add(AbstractEpollChannel ch) throws IOException { + assert inEventLoop(); + int fd = ch.socket.intValue(); + Native.epollCtlAdd(epollFd.intValue(), fd, ch.flags); + AbstractEpollChannel old = channels.put(fd, ch); + + // We either expect to have no Channel in the map with the same FD or that the FD of the old Channel is already + // closed. + assert old == null || !old.isOpen(); + } + + /** + * The flags of the given epoll was modified so update the registration + */ + void modify(AbstractEpollChannel ch) throws IOException { + assert inEventLoop(); + Native.epollCtlMod(epollFd.intValue(), ch.socket.intValue(), ch.flags); + } + + /** + * Deregister the given epoll from this {@link EventLoop}. + */ + void remove(AbstractEpollChannel ch) throws IOException { + assert inEventLoop(); + int fd = ch.socket.intValue(); + + AbstractEpollChannel old = channels.remove(fd); + if (old != null && old != ch) { + // The Channel mapping was already replaced due FD reuse, put back the stored Channel. + channels.put(fd, old); + + // If we found another Channel in the map that is mapped to the same FD the given Channel MUST be closed. + assert !ch.isOpen(); + } else if (ch.isOpen()) { + // Remove the epoll. This is only needed if it's still open as otherwise it will be automatically + // removed once the file-descriptor is closed. + Native.epollCtlDel(epollFd.intValue(), fd); + } + } + + @Override + protected Queue newTaskQueue(int maxPendingTasks) { + return newTaskQueue0(maxPendingTasks); + } + + private static Queue newTaskQueue0(int maxPendingTasks) { + // This event loop never calls takeTask() + return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.newMpscQueue() + : PlatformDependent.newMpscQueue(maxPendingTasks); + } + + /** + * Returns the percentage of the desired amount of time spent for I/O in the event loop. + */ + public int getIoRatio() { + return ioRatio; + } + + /** + * Sets the percentage of the desired amount of time spent for I/O in the event loop. The default value is + * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks. + */ + public void setIoRatio(int ioRatio) { + if (ioRatio <= 0 || ioRatio > 100) { + throw new IllegalArgumentException("ioRatio: " + ioRatio + " (expected: 0 < ioRatio <= 100)"); + } + this.ioRatio = ioRatio; + } + + @Override + public int registeredChannels() { + return channels.size(); + } + + @Override + public Iterator registeredChannelsIterator() { + assert inEventLoop(); + IntObjectMap ch = channels; + if (ch.isEmpty()) { + return ChannelsReadOnlyIterator.empty(); + } + return new ChannelsReadOnlyIterator(ch.values()); + } + + private long epollWait(long deadlineNanos) throws IOException { + if (deadlineNanos == NONE) { + return Native.epollWait(epollFd, events, timerFd, + Integer.MAX_VALUE, 0, EPOLL_WAIT_MILLIS_THRESHOLD); // disarm timer + } + long totalDelay = deadlineToDelayNanos(deadlineNanos); + int delaySeconds = (int) min(totalDelay / 1000000000L, Integer.MAX_VALUE); + int delayNanos = (int) min(totalDelay - delaySeconds * 1000000000L, MAX_SCHEDULED_TIMERFD_NS); + return Native.epollWait(epollFd, events, timerFd, delaySeconds, delayNanos, EPOLL_WAIT_MILLIS_THRESHOLD); + } + + private int epollWaitNoTimerChange() throws IOException { + return Native.epollWait(epollFd, events, false); + } + + private int epollWaitNow() throws IOException { + return Native.epollWait(epollFd, events, true); + } + + private int epollBusyWait() throws IOException { + return Native.epollBusyWait(epollFd, events); + } + + private int epollWaitTimeboxed() throws IOException { + // Wait with 1 second "safeguard" timeout + return Native.epollWait(epollFd, events, 1000); + } + + @Override + protected void run() { + long prevDeadlineNanos = NONE; + for (;;) { + try { + int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); + switch (strategy) { + case SelectStrategy.CONTINUE: + continue; + + case SelectStrategy.BUSY_WAIT: + strategy = epollBusyWait(); + break; + + case SelectStrategy.SELECT: + if (pendingWakeup) { + // We are going to be immediately woken so no need to reset wakenUp + // or check for timerfd adjustment. + strategy = epollWaitTimeboxed(); + if (strategy != 0) { + break; + } + // We timed out so assume that we missed the write event due to an + // abnormally failed syscall (the write itself or a prior epoll_wait) + logger.warn("Missed eventfd write (not seen after > 1 second)"); + pendingWakeup = false; + if (hasTasks()) { + break; + } + // fall-through + } + + long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); + if (curDeadlineNanos == -1L) { + curDeadlineNanos = NONE; // nothing on the calendar + } + nextWakeupNanos.set(curDeadlineNanos); + try { + if (!hasTasks()) { + if (curDeadlineNanos == prevDeadlineNanos) { + // No timer activity needed + strategy = epollWaitNoTimerChange(); + } else { + // Timerfd needs to be re-armed or disarmed + long result = epollWait(curDeadlineNanos); + // The result contains the actual return value and if a timer was used or not. + // We need to "unpack" using the helper methods exposed in Native. + strategy = Native.epollReady(result); + prevDeadlineNanos = Native.epollTimerWasUsed(result) ? curDeadlineNanos : NONE; + } + } + } finally { + // Try get() first to avoid much more expensive CAS in the case we + // were woken via the wakeup() method (submitted task) + if (nextWakeupNanos.get() == AWAKE || nextWakeupNanos.getAndSet(AWAKE) == AWAKE) { + pendingWakeup = true; + } + } + // fallthrough + default: + } + + final int ioRatio = this.ioRatio; + if (ioRatio == 100) { + try { + if (strategy > 0 && processReady(events, strategy)) { + prevDeadlineNanos = NONE; + } + } finally { + // Ensure we always run tasks. + runAllTasks(); + } + } else if (strategy > 0) { + final long ioStartTime = System.nanoTime(); + try { + if (processReady(events, strategy)) { + prevDeadlineNanos = NONE; + } + } finally { + // Ensure we always run tasks. + final long ioTime = System.nanoTime() - ioStartTime; + runAllTasks(ioTime * (100 - ioRatio) / ioRatio); + } + } else { + runAllTasks(0); // This will run the minimum number of tasks + } + if (allowGrowing && strategy == events.length()) { + //increase the size of the array as we needed the whole space for the events + events.increase(); + } + } catch (Error e) { + throw e; + } catch (Throwable t) { + handleLoopException(t); + } finally { + // Always handle shutdown even if the loop processing threw an exception. + try { + if (isShuttingDown()) { + closeAll(); + if (confirmShutdown()) { + break; + } + } + } catch (Error e) { + throw e; + } catch (Throwable t) { + handleLoopException(t); + } + } + } + } + + /** + * Visible only for testing! + */ + void handleLoopException(Throwable t) { + logger.warn("Unexpected exception in the selector loop.", t); + + // Prevent possible consecutive immediate failures that lead to + // excessive CPU consumption. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Ignore. + } + } + + private void closeAll() { + // Using the intermediate collection to prevent ConcurrentModificationException. + // In the `close()` method, the channel is deleted from `channels` map. + AbstractEpollChannel[] localChannels = channels.values().toArray(new AbstractEpollChannel[0]); + + for (AbstractEpollChannel ch: localChannels) { + ch.unsafe().close(ch.unsafe().voidPromise()); + } + } + + // Returns true if a timerFd event was encountered + private boolean processReady(EpollEventArray events, int ready) { + boolean timerFired = false; + for (int i = 0; i < ready; i ++) { + final int fd = events.fd(i); + if (fd == eventFd.intValue()) { + pendingWakeup = false; + } else if (fd == timerFd.intValue()) { + timerFired = true; + } else { + final long ev = events.events(i); + + AbstractEpollChannel ch = channels.get(fd); + if (ch != null) { + // Don't change the ordering of processing EPOLLOUT | EPOLLRDHUP / EPOLLIN if you're not 100% + // sure about it! + // Re-ordering can easily introduce bugs and bad side-effects, as we found out painfully in the + // past. + AbstractEpollUnsafe unsafe = (AbstractEpollUnsafe) ch.unsafe(); + + // First check for EPOLLOUT as we may need to fail the connect ChannelPromise before try + // to read from the file descriptor. + // See https://github.com/netty/netty/issues/3785 + // + // It is possible for an EPOLLOUT or EPOLLERR to be generated when a connection is refused. + // In either case epollOutReady() will do the correct thing (finish connecting, or fail + // the connection). + // See https://github.com/netty/netty/issues/3848 + if ((ev & (Native.EPOLLERR | Native.EPOLLOUT)) != 0) { + // Force flush of data as the epoll is writable again + unsafe.epollOutReady(); + } + + // Check EPOLLIN before EPOLLRDHUP to ensure all data is read before shutting down the input. + // See https://github.com/netty/netty/issues/4317. + // + // If EPOLLIN or EPOLLERR was received and the channel is still open call epollInReady(). This will + // try to read from the underlying file descriptor and so notify the user about the error. + if ((ev & (Native.EPOLLERR | Native.EPOLLIN)) != 0) { + // The Channel is still open and there is something to read. Do it now. + unsafe.epollInReady(); + } + + // Check if EPOLLRDHUP was set, this will notify us for connection-reset in which case + // we may close the channel directly or try to read more data depending on the state of the + // Channel and als depending on the AbstractEpollChannel subtype. + if ((ev & Native.EPOLLRDHUP) != 0) { + unsafe.epollRdHupReady(); + } + } else { + // We received an event for an fd which we not use anymore. Remove it from the epoll_event set. + try { + Native.epollCtlDel(epollFd.intValue(), fd); + } catch (IOException ignore) { + // This can happen but is nothing we need to worry about as we only try to delete + // the fd from the epoll set as we not found it in our mappings. So this call to + // epollCtlDel(...) is just to ensure we cleanup stuff and so may fail if it was + // deleted before or the file descriptor was closed before. + } + } + } + } + return timerFired; + } + + @Override + protected void cleanup() { + try { + closeFileDescriptors(); + } finally { + // release native memory + if (iovArray != null) { + iovArray.release(); + iovArray = null; + } + if (datagramPacketArray != null) { + datagramPacketArray.release(); + datagramPacketArray = null; + } + events.free(); + } + } + + /** + * This method is intended for use by process checkpoint/restore + * integration, such as OpenJDK CRaC. + * It's up to the caller to ensure that there is no concurrent use + * of the FDs while these are closed, e.g. by blocking the executor. + */ + @UnstableApi + public void closeFileDescriptors() { + // Ensure any in-flight wakeup writes have been performed prior to closing eventFd. + while (pendingWakeup) { + try { + int count = epollWaitTimeboxed(); + if (count == 0) { + // We timed-out so assume that the write we're expecting isn't coming + break; + } + for (int i = 0; i < count; i++) { + if (events.fd(i) == eventFd.intValue()) { + pendingWakeup = false; + break; + } + } + } catch (IOException ignore) { + // ignore + } + } + try { + eventFd.close(); + } catch (IOException e) { + logger.warn("Failed to close the event fd.", e); + } + try { + timerFd.close(); + } catch (IOException e) { + logger.warn("Failed to close the timer fd.", e); + } + + try { + epollFd.close(); + } catch (IOException e) { + logger.warn("Failed to close the epoll fd.", e); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java new file mode 100644 index 0000000..a749514 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java @@ -0,0 +1,193 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.channel.DefaultSelectStrategyFactory; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.EventLoopTaskQueueFactory; +import io.netty.channel.MultithreadEventLoopGroup; +import io.netty.channel.SelectStrategyFactory; +import io.netty.channel.SingleThreadEventLoop; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.EventExecutorChooserFactory; +import io.netty.util.concurrent.RejectedExecutionHandler; +import io.netty.util.concurrent.RejectedExecutionHandlers; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; + +/** + * {@link EventLoopGroup} which uses epoll under the covers. Because of this + * it only works on linux. + */ +public final class EpollEventLoopGroup extends MultithreadEventLoopGroup { + + // This does not use static by design to ensure the class can be loaded and only do the check when its actually + // instanced. + { + // Ensure JNI is initialized by the time this class is loaded. + Epoll.ensureAvailability(); + } + + /** + * Create a new instance using the default number of threads and the default {@link ThreadFactory}. + */ + public EpollEventLoopGroup() { + this(0); + } + + /** + * Create a new instance using the specified number of threads and the default {@link ThreadFactory}. + */ + public EpollEventLoopGroup(int nThreads) { + this(nThreads, (ThreadFactory) null); + } + + /** + * Create a new instance using the default number of threads and the given {@link ThreadFactory}. + */ + @SuppressWarnings("deprecation") + public EpollEventLoopGroup(ThreadFactory threadFactory) { + this(0, threadFactory, 0); + } + + /** + * Create a new instance using the specified number of threads and the default {@link ThreadFactory}. + */ + @SuppressWarnings("deprecation") + public EpollEventLoopGroup(int nThreads, SelectStrategyFactory selectStrategyFactory) { + this(nThreads, (ThreadFactory) null, selectStrategyFactory); + } + + /** + * Create a new instance using the specified number of threads and the given {@link ThreadFactory}. + */ + @SuppressWarnings("deprecation") + public EpollEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + this(nThreads, threadFactory, 0); + } + + public EpollEventLoopGroup(int nThreads, Executor executor) { + this(nThreads, executor, DefaultSelectStrategyFactory.INSTANCE); + } + + /** + * Create a new instance using the specified number of threads and the given {@link ThreadFactory}. + */ + @SuppressWarnings("deprecation") + public EpollEventLoopGroup(int nThreads, ThreadFactory threadFactory, SelectStrategyFactory selectStrategyFactory) { + this(nThreads, threadFactory, 0, selectStrategyFactory); + } + + /** + * Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the given + * maximal amount of epoll events to handle per epollWait(...). + * + * @deprecated Use {@link #EpollEventLoopGroup(int)} or {@link #EpollEventLoopGroup(int, ThreadFactory)} + */ + @Deprecated + public EpollEventLoopGroup(int nThreads, ThreadFactory threadFactory, int maxEventsAtOnce) { + this(nThreads, threadFactory, maxEventsAtOnce, DefaultSelectStrategyFactory.INSTANCE); + } + + /** + * Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the given + * maximal amount of epoll events to handle per epollWait(...). + * + * @deprecated Use {@link #EpollEventLoopGroup(int)}, {@link #EpollEventLoopGroup(int, ThreadFactory)}, or + * {@link #EpollEventLoopGroup(int, SelectStrategyFactory)} + */ + @Deprecated + public EpollEventLoopGroup(int nThreads, ThreadFactory threadFactory, int maxEventsAtOnce, + SelectStrategyFactory selectStrategyFactory) { + super(nThreads, threadFactory, maxEventsAtOnce, selectStrategyFactory, RejectedExecutionHandlers.reject()); + } + + public EpollEventLoopGroup(int nThreads, Executor executor, SelectStrategyFactory selectStrategyFactory) { + super(nThreads, executor, 0, selectStrategyFactory, RejectedExecutionHandlers.reject()); + } + + public EpollEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + SelectStrategyFactory selectStrategyFactory) { + super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, RejectedExecutionHandlers.reject()); + } + + public EpollEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + SelectStrategyFactory selectStrategyFactory, + RejectedExecutionHandler rejectedExecutionHandler) { + super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler); + } + + public EpollEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + SelectStrategyFactory selectStrategyFactory, + RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory queueFactory) { + super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler, queueFactory); + } + + /** + * @param nThreads the number of threads that will be used by this instance. + * @param executor the Executor to use, or {@code null} if default one should be used. + * @param chooserFactory the {@link EventExecutorChooserFactory} to use. + * @param selectStrategyFactory the {@link SelectStrategyFactory} to use. + * @param rejectedExecutionHandler the {@link RejectedExecutionHandler} to use. + * @param taskQueueFactory the {@link EventLoopTaskQueueFactory} to use for + * {@link SingleThreadEventLoop#execute(Runnable)}, + * or {@code null} if default one should be used. + * @param tailTaskQueueFactory the {@link EventLoopTaskQueueFactory} to use for + * {@link SingleThreadEventLoop#executeAfterEventLoopIteration(Runnable)}, + * or {@code null} if default one should be used. + */ + public EpollEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + SelectStrategyFactory selectStrategyFactory, + RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory taskQueueFactory, + EventLoopTaskQueueFactory tailTaskQueueFactory) { + super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler, taskQueueFactory, + tailTaskQueueFactory); + } + + /** + * Sets the percentage of the desired amount of time spent for I/O in the child event loops. The default value is + * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks. + */ + public void setIoRatio(int ioRatio) { + for (EventExecutor e: this) { + ((EpollEventLoop) e).setIoRatio(ioRatio); + } + } + + @Override + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + Integer maxEvents = (Integer) args[0]; + SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1]; + RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2]; + EventLoopTaskQueueFactory taskQueueFactory = null; + EventLoopTaskQueueFactory tailTaskQueueFactory = null; + + int argsLength = args.length; + if (argsLength > 3) { + taskQueueFactory = (EventLoopTaskQueueFactory) args[3]; + } + if (argsLength > 4) { + tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4]; + } + return new EpollEventLoop(this, executor, maxEvents, + selectStrategyFactory.newSelectStrategy(), + rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory); + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollMode.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollMode.java new file mode 100644 index 0000000..32474a6 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollMode.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 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.channel.epoll; + +/** + * The epoll mode to use. + */ +public enum EpollMode { + + /** + * Use {@code EPOLLET} (edge-triggered). + * + * @see man 7 epoll. + */ + EDGE_TRIGGERED, + + /** + * Do not use {@code EPOLLET} (level-triggered). + * + * @see man 7 epoll. + */ + LEVEL_TRIGGERED +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java new file mode 100644 index 0000000..dc5a0b9 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.RecvByteBufAllocator.DelegatingHandle; +import io.netty.channel.RecvByteBufAllocator.ExtendedHandle; +import io.netty.channel.unix.PreferredDirectByteBufAllocator; +import io.netty.util.UncheckedBooleanSupplier; + +class EpollRecvByteAllocatorHandle extends DelegatingHandle implements ExtendedHandle { + private final PreferredDirectByteBufAllocator preferredDirectByteBufAllocator = + new PreferredDirectByteBufAllocator(); + private final UncheckedBooleanSupplier defaultMaybeMoreDataSupplier = new UncheckedBooleanSupplier() { + @Override + public boolean get() { + return maybeMoreDataToRead(); + } + }; + private boolean isEdgeTriggered; + private boolean receivedRdHup; + + EpollRecvByteAllocatorHandle(ExtendedHandle handle) { + super(handle); + } + + final void receivedRdHup() { + receivedRdHup = true; + } + + final boolean isReceivedRdHup() { + return receivedRdHup; + } + + boolean maybeMoreDataToRead() { + /** + * EPOLL ET requires that we read until we get an EAGAIN + * (see Q9 in epoll man). However in order to + * respect auto read we supporting reading to stop if auto read is off. It is expected that the + * {@link #EpollSocketChannel} implementations will track if we are in edgeTriggered mode and all data was not + * read, and will force a EPOLLIN ready event. + * + * It is assumed RDHUP is handled externally by checking {@link #isReceivedRdHup()}. + */ + return (isEdgeTriggered && lastBytesRead() > 0) || + (!isEdgeTriggered && lastBytesRead() == attemptedBytesRead()); + } + + final void edgeTriggered(boolean edgeTriggered) { + isEdgeTriggered = edgeTriggered; + } + + final boolean isEdgeTriggered() { + return isEdgeTriggered; + } + + @Override + public final ByteBuf allocate(ByteBufAllocator alloc) { + // We need to ensure we always allocate a direct ByteBuf as we can only use a direct buffer to read via JNI. + preferredDirectByteBufAllocator.updateAllocator(alloc); + return delegate().allocate(preferredDirectByteBufAllocator); + } + + @Override + public final boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) { + return ((ExtendedHandle) delegate()).continueReading(maybeMoreDataSupplier); + } + + @Override + public final boolean continueReading() { + // We must override the supplier which determines if there maybe more data to read. + return continueReading(defaultMaybeMoreDataSupplier); + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java new file mode 100644 index 0000000..0a43992 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.channel.RecvByteBufAllocator; + +final class EpollRecvByteAllocatorStreamingHandle extends EpollRecvByteAllocatorHandle { + EpollRecvByteAllocatorStreamingHandle(RecvByteBufAllocator.ExtendedHandle handle) { + super(handle); + } + + @Override + boolean maybeMoreDataToRead() { + /** + * For stream oriented descriptors we can assume we are done reading if the last read attempt didn't produce + * a full buffer (see Q9 in epoll man). + * + * If EPOLLRDHUP has been received we must read until we get a read error. + */ + return lastBytesRead() == attemptedBytesRead() || isReceivedRdHup(); + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java new file mode 100644 index 0000000..1a418ec --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java @@ -0,0 +1,234 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelOption; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.ServerChannelRecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.ServerSocketChannelConfig; +import io.netty.util.NetUtil; + +import java.io.IOException; +import java.util.Map; + +import static io.netty.channel.ChannelOption.SO_BACKLOG; +import static io.netty.channel.ChannelOption.SO_RCVBUF; +import static io.netty.channel.ChannelOption.SO_REUSEADDR; +import static io.netty.channel.ChannelOption.TCP_FASTOPEN; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +public class EpollServerChannelConfig extends EpollChannelConfig implements ServerSocketChannelConfig { + private volatile int backlog = NetUtil.SOMAXCONN; + private volatile int pendingFastOpenRequestsThreshold; + + EpollServerChannelConfig(AbstractEpollChannel channel) { + super(channel, new ServerChannelRecvByteBufAllocator()); + } + + @Override + public Map, Object> getOptions() { + return getOptions(super.getOptions(), SO_RCVBUF, SO_REUSEADDR, SO_BACKLOG, TCP_FASTOPEN); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == SO_RCVBUF) { + return (T) Integer.valueOf(getReceiveBufferSize()); + } + if (option == SO_REUSEADDR) { + return (T) Boolean.valueOf(isReuseAddress()); + } + if (option == SO_BACKLOG) { + return (T) Integer.valueOf(getBacklog()); + } + if (option == TCP_FASTOPEN) { + return (T) Integer.valueOf(getTcpFastopen()); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == SO_RCVBUF) { + setReceiveBufferSize((Integer) value); + } else if (option == SO_REUSEADDR) { + setReuseAddress((Boolean) value); + } else if (option == SO_BACKLOG) { + setBacklog((Integer) value); + } else if (option == TCP_FASTOPEN) { + setTcpFastopen((Integer) value); + } else { + return super.setOption(option, value); + } + + return true; + } + + @Override + public boolean isReuseAddress() { + try { + return ((AbstractEpollChannel) channel).socket.isReuseAddress(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollServerChannelConfig setReuseAddress(boolean reuseAddress) { + try { + ((AbstractEpollChannel) channel).socket.setReuseAddress(reuseAddress); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getReceiveBufferSize() { + try { + return ((AbstractEpollChannel) channel).socket.getReceiveBufferSize(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollServerChannelConfig setReceiveBufferSize(int receiveBufferSize) { + try { + ((AbstractEpollChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getBacklog() { + return backlog; + } + + @Override + public EpollServerChannelConfig setBacklog(int backlog) { + checkPositiveOrZero(backlog, "backlog"); + this.backlog = backlog; + return this; + } + + /** + * Returns threshold value of number of pending for fast open connect. + * + * @see RFC 7413 Passive Open + */ + public int getTcpFastopen() { + return pendingFastOpenRequestsThreshold; + } + + /** + * Enables tcpFastOpen on the server channel. If the underlying os doesn't support TCP_FASTOPEN setting this has no + * effect. This has to be set before doing listen on the socket otherwise this takes no effect. + * + * @param pendingFastOpenRequestsThreshold number of requests to be pending for fastopen at a given point in time + * for security. + * + * @see RFC 7413 Passive Open + */ + public EpollServerChannelConfig setTcpFastopen(int pendingFastOpenRequestsThreshold) { + this.pendingFastOpenRequestsThreshold = checkPositiveOrZero(pendingFastOpenRequestsThreshold, + "pendingFastOpenRequestsThreshold"); + return this; + } + + @Override + public EpollServerChannelConfig setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + return this; + } + + @Override + public EpollServerChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + @Deprecated + public EpollServerChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public EpollServerChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public EpollServerChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public EpollServerChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public EpollServerChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + @Deprecated + public EpollServerChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + @Deprecated + public EpollServerChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public EpollServerChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public EpollServerChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + @Override + public EpollServerChannelConfig setEpollMode(EpollMode mode) { + super.setEpollMode(mode); + return this; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerDomainSocketChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerDomainSocketChannel.java new file mode 100644 index 0000000..0f06c7f --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerDomainSocketChannel.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015 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.channel.epoll; + +import io.netty.channel.Channel; +import io.netty.channel.unix.DomainSocketAddress; +import io.netty.channel.unix.ServerDomainSocketChannel; +import io.netty.channel.unix.Socket; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.File; +import java.net.SocketAddress; + +import static io.netty.channel.epoll.LinuxSocket.newSocketDomain; + +public final class EpollServerDomainSocketChannel extends AbstractEpollServerChannel + implements ServerDomainSocketChannel { + private static final InternalLogger logger = InternalLoggerFactory.getInstance( + EpollServerDomainSocketChannel.class); + + private final EpollServerChannelConfig config = new EpollServerChannelConfig(this); + private volatile DomainSocketAddress local; + + public EpollServerDomainSocketChannel() { + super(newSocketDomain(), false); + } + + public EpollServerDomainSocketChannel(int fd) { + super(fd); + } + + EpollServerDomainSocketChannel(LinuxSocket fd) { + super(fd); + } + + EpollServerDomainSocketChannel(LinuxSocket fd, boolean active) { + super(fd, active); + } + + @Override + protected Channel newChildChannel(int fd, byte[] addr, int offset, int len) throws Exception { + return new EpollDomainSocketChannel(this, new Socket(fd)); + } + + @Override + protected DomainSocketAddress localAddress0() { + return local; + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + socket.bind(localAddress); + socket.listen(config.getBacklog()); + local = (DomainSocketAddress) localAddress; + active = true; + } + + @Override + protected void doClose() throws Exception { + try { + super.doClose(); + } finally { + DomainSocketAddress local = this.local; + if (local != null) { + // Delete the socket file if possible. + File socketFile = new File(local.path()); + boolean success = socketFile.delete(); + if (!success && logger.isDebugEnabled()) { + logger.debug("Failed to delete a domain socket file: {}", local.path()); + } + } + } + } + + @Override + public EpollServerChannelConfig config() { + return config; + } + + @Override + public DomainSocketAddress remoteAddress() { + return (DomainSocketAddress) super.remoteAddress(); + } + + @Override + public DomainSocketAddress localAddress() { + return (DomainSocketAddress) super.localAddress(); + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java new file mode 100644 index 0000000..cc806b1 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java @@ -0,0 +1,115 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.ServerSocketChannel; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import static io.netty.channel.epoll.LinuxSocket.newSocketStream; +import static io.netty.channel.epoll.Native.IS_SUPPORTING_TCP_FASTOPEN_SERVER; +import static io.netty.channel.unix.NativeInetAddress.address; + +/** + * {@link ServerSocketChannel} implementation that uses linux EPOLL Edge-Triggered Mode for + * maximal performance. + */ +public final class EpollServerSocketChannel extends AbstractEpollServerChannel implements ServerSocketChannel { + + private final EpollServerSocketChannelConfig config; + private volatile Collection tcpMd5SigAddresses = Collections.emptyList(); + + public EpollServerSocketChannel() { + this((InternetProtocolFamily) null); + } + + public EpollServerSocketChannel(InternetProtocolFamily protocol) { + super(newSocketStream(protocol), false); + config = new EpollServerSocketChannelConfig(this); + } + + public EpollServerSocketChannel(int fd) { + // Must call this constructor to ensure this object's local address is configured correctly. + // The local address can only be obtained from a Socket object. + this(new LinuxSocket(fd)); + } + + EpollServerSocketChannel(LinuxSocket fd) { + super(fd); + config = new EpollServerSocketChannelConfig(this); + } + + EpollServerSocketChannel(LinuxSocket fd, boolean active) { + super(fd, active); + config = new EpollServerSocketChannelConfig(this); + } + + @Override + protected boolean isCompatible(EventLoop loop) { + return loop instanceof EpollEventLoop; + } + + @Override + protected void doBind(SocketAddress localAddress) throws Exception { + super.doBind(localAddress); + final int tcpFastopen; + if (IS_SUPPORTING_TCP_FASTOPEN_SERVER && (tcpFastopen = config.getTcpFastopen()) > 0) { + socket.setTcpFastOpen(tcpFastopen); + } + socket.listen(config.getBacklog()); + active = true; + } + + @Override + public InetSocketAddress remoteAddress() { + return (InetSocketAddress) super.remoteAddress(); + } + + @Override + public InetSocketAddress localAddress() { + return (InetSocketAddress) super.localAddress(); + } + + @Override + public EpollServerSocketChannelConfig config() { + return config; + } + + @Override + protected Channel newChildChannel(int fd, byte[] address, int offset, int len) throws Exception { + return new EpollSocketChannel(this, new LinuxSocket(fd), address(address, offset, len)); + } + + Collection tcpMd5SigAddresses() { + return tcpMd5SigAddresses; + } + + void setTcpMd5Sig(Map keys) throws IOException { + // Add synchronized as newTcpMp5Sigs might do multiple operations on the socket itself. + synchronized (this) { + tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannelConfig.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannelConfig.java new file mode 100644 index 0000000..014604e --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannelConfig.java @@ -0,0 +1,288 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelOption; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.ServerSocketChannelConfig; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Map; + +public final class EpollServerSocketChannelConfig extends EpollServerChannelConfig + implements ServerSocketChannelConfig { + + EpollServerSocketChannelConfig(EpollServerSocketChannel channel) { + super(channel); + + // Use SO_REUSEADDR by default as java.nio does the same. + // + // See https://github.com/netty/netty/issues/2605 + setReuseAddress(true); + } + + @Override + public Map, Object> getOptions() { + return getOptions(super.getOptions(), EpollChannelOption.SO_REUSEPORT, EpollChannelOption.IP_FREEBIND, + EpollChannelOption.IP_TRANSPARENT, EpollChannelOption.TCP_DEFER_ACCEPT); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == EpollChannelOption.SO_REUSEPORT) { + return (T) Boolean.valueOf(isReusePort()); + } + if (option == EpollChannelOption.IP_FREEBIND) { + return (T) Boolean.valueOf(isFreeBind()); + } + if (option == EpollChannelOption.IP_TRANSPARENT) { + return (T) Boolean.valueOf(isIpTransparent()); + } + if (option == EpollChannelOption.TCP_DEFER_ACCEPT) { + return (T) Integer.valueOf(getTcpDeferAccept()); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == EpollChannelOption.SO_REUSEPORT) { + setReusePort((Boolean) value); + } else if (option == EpollChannelOption.IP_FREEBIND) { + setFreeBind((Boolean) value); + } else if (option == EpollChannelOption.IP_TRANSPARENT) { + setIpTransparent((Boolean) value); + } else if (option == EpollChannelOption.TCP_MD5SIG) { + @SuppressWarnings("unchecked") + final Map m = (Map) value; + setTcpMd5Sig(m); + } else if (option == EpollChannelOption.TCP_DEFER_ACCEPT) { + setTcpDeferAccept((Integer) value); + } else { + return super.setOption(option, value); + } + + return true; + } + + @Override + public EpollServerSocketChannelConfig setReuseAddress(boolean reuseAddress) { + super.setReuseAddress(reuseAddress); + return this; + } + + @Override + public EpollServerSocketChannelConfig setReceiveBufferSize(int receiveBufferSize) { + super.setReceiveBufferSize(receiveBufferSize); + return this; + } + + @Override + public EpollServerSocketChannelConfig setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + return this; + } + + @Override + public EpollServerSocketChannelConfig setBacklog(int backlog) { + super.setBacklog(backlog); + return this; + } + + @Override + public EpollServerSocketChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + @Deprecated + public EpollServerSocketChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public EpollServerSocketChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public EpollServerSocketChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public EpollServerSocketChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public EpollServerSocketChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + @Deprecated + public EpollServerSocketChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + @Deprecated + public EpollServerSocketChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public EpollServerSocketChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public EpollServerSocketChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + /** + * Set the {@code TCP_MD5SIG} option on the socket. See {@code linux/tcp.h} for more details. + * Keys can only be set on, not read to prevent a potential leak, as they are confidential. + * Allowing them being read would mean anyone with access to the channel could get them. + */ + public EpollServerSocketChannelConfig setTcpMd5Sig(Map keys) { + try { + ((EpollServerSocketChannel) channel).setTcpMd5Sig(keys); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if the SO_REUSEPORT option is set. + */ + public boolean isReusePort() { + try { + return ((EpollServerSocketChannel) channel).socket.isReusePort(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the SO_REUSEPORT option on the underlying Channel. This will allow to bind multiple + * {@link EpollSocketChannel}s to the same port and so accept connections with multiple threads. + * + * Be aware this method needs be called before {@link EpollSocketChannel#bind(java.net.SocketAddress)} to have + * any affect. + */ + public EpollServerSocketChannelConfig setReusePort(boolean reusePort) { + try { + ((EpollServerSocketChannel) channel).socket.setReusePort(reusePort); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if IP_FREEBIND is enabled, + * {@code false} otherwise. + */ + public boolean isFreeBind() { + try { + return ((EpollServerSocketChannel) channel).socket.isIpFreeBind(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * If {@code true} is used IP_FREEBIND is enabled, + * {@code false} for disable it. Default is disabled. + */ + public EpollServerSocketChannelConfig setFreeBind(boolean freeBind) { + try { + ((EpollServerSocketChannel) channel).socket.setIpFreeBind(freeBind); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if IP_TRANSPARENT is enabled, + * {@code false} otherwise. + */ + public boolean isIpTransparent() { + try { + return ((EpollServerSocketChannel) channel).socket.isIpTransparent(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * If {@code true} is used IP_TRANSPARENT is enabled, + * {@code false} for disable it. Default is disabled. + */ + public EpollServerSocketChannelConfig setIpTransparent(boolean transparent) { + try { + ((EpollServerSocketChannel) channel).socket.setIpTransparent(transparent); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_DEFER_ACCEPT} option on the socket. See {@code man 7 tcp} for more details. + */ + public EpollServerSocketChannelConfig setTcpDeferAccept(int deferAccept) { + try { + ((EpollServerSocketChannel) channel).socket.setTcpDeferAccept(deferAccept); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns a positive value if TCP_DEFER_ACCEPT is enabled. + */ + public int getTcpDeferAccept() { + try { + return ((EpollServerSocketChannel) channel).socket.getTcpDeferAccept(); + } catch (IOException e) { + throw new ChannelException(e); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java new file mode 100644 index 0000000..e9559fd --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java @@ -0,0 +1,176 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import io.netty.util.concurrent.GlobalEventExecutor; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Executor; + +import static io.netty.channel.epoll.LinuxSocket.newSocketStream; +import static io.netty.channel.epoll.Native.IS_SUPPORTING_TCP_FASTOPEN_CLIENT; + +/** + * {@link SocketChannel} implementation that uses linux EPOLL Edge-Triggered Mode for + * maximal performance. + */ +public final class EpollSocketChannel extends AbstractEpollStreamChannel implements SocketChannel { + + private final EpollSocketChannelConfig config; + + private volatile Collection tcpMd5SigAddresses = Collections.emptyList(); + + public EpollSocketChannel() { + super(newSocketStream(), false); + config = new EpollSocketChannelConfig(this); + } + + public EpollSocketChannel(InternetProtocolFamily protocol) { + super(newSocketStream(protocol), false); + config = new EpollSocketChannelConfig(this); + } + + public EpollSocketChannel(int fd) { + super(fd); + config = new EpollSocketChannelConfig(this); + } + + EpollSocketChannel(LinuxSocket fd, boolean active) { + super(fd, active); + config = new EpollSocketChannelConfig(this); + } + + EpollSocketChannel(Channel parent, LinuxSocket fd, InetSocketAddress remoteAddress) { + super(parent, fd, remoteAddress); + config = new EpollSocketChannelConfig(this); + + if (parent instanceof EpollServerSocketChannel) { + tcpMd5SigAddresses = ((EpollServerSocketChannel) parent).tcpMd5SigAddresses(); + } + } + + /** + * Returns the {@code TCP_INFO} for the current socket. + * See man 7 tcp. + */ + public EpollTcpInfo tcpInfo() { + return tcpInfo(new EpollTcpInfo()); + } + + /** + * Updates and returns the {@code TCP_INFO} for the current socket. + * See man 7 tcp. + */ + public EpollTcpInfo tcpInfo(EpollTcpInfo info) { + try { + socket.getTcpInfo(info); + return info; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public InetSocketAddress remoteAddress() { + return (InetSocketAddress) super.remoteAddress(); + } + + @Override + public InetSocketAddress localAddress() { + return (InetSocketAddress) super.localAddress(); + } + + @Override + public EpollSocketChannelConfig config() { + return config; + } + + @Override + public ServerSocketChannel parent() { + return (ServerSocketChannel) super.parent(); + } + + @Override + protected AbstractEpollUnsafe newUnsafe() { + return new EpollSocketChannelUnsafe(); + } + + @Override + boolean doConnect0(SocketAddress remote) throws Exception { + if (IS_SUPPORTING_TCP_FASTOPEN_CLIENT && config.isTcpFastOpenConnect()) { + ChannelOutboundBuffer outbound = unsafe().outboundBuffer(); + outbound.addFlush(); + Object curr; + if ((curr = outbound.current()) instanceof ByteBuf) { + ByteBuf initialData = (ByteBuf) curr; + // If no cookie is present, the write fails with EINPROGRESS and this call basically + // becomes a normal async connect. All writes will be sent normally afterwards. + long localFlushedAmount = doWriteOrSendBytes( + initialData, (InetSocketAddress) remote, true); + if (localFlushedAmount > 0) { + // We had a cookie and our fast-open proceeded. Remove written data + // then continue with normal TCP operation. + outbound.removeBytes(localFlushedAmount); + return true; + } + } + } + return super.doConnect0(remote); + } + + private final class EpollSocketChannelUnsafe extends EpollStreamUnsafe { + @Override + protected Executor prepareToClose() { + try { + // Check isOpen() first as otherwise it will throw a RuntimeException + // when call getSoLinger() as the fd is not valid anymore. + if (isOpen() && config().getSoLinger() > 0) { + // We need to cancel this key of the channel so we may not end up in a eventloop spin + // because we try to read or write until the actual close happens which may be later due + // SO_LINGER handling. + // See https://github.com/netty/netty/issues/4449 + ((EpollEventLoop) eventLoop()).remove(EpollSocketChannel.this); + return GlobalEventExecutor.INSTANCE; + } + } catch (Throwable ignore) { + // Ignore the error as the underlying channel may be closed in the meantime and so + // getSoLinger() may produce an exception. In this case we just return null. + // See https://github.com/netty/netty/issues/4449 + } + return null; + } + } + + void setTcpMd5Sig(Map keys) throws IOException { + // Add synchronized as newTcpMp5Sigs might do multiple operations on the socket itself. + synchronized (this) { + tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java new file mode 100644 index 0000000..c4c899b --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java @@ -0,0 +1,665 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelException; +import io.netty.channel.ChannelOption; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.SocketChannelConfig; +import io.netty.util.internal.PlatformDependent; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Map; + +import static io.netty.channel.ChannelOption.ALLOW_HALF_CLOSURE; +import static io.netty.channel.ChannelOption.IP_TOS; +import static io.netty.channel.ChannelOption.SO_KEEPALIVE; +import static io.netty.channel.ChannelOption.SO_LINGER; +import static io.netty.channel.ChannelOption.SO_RCVBUF; +import static io.netty.channel.ChannelOption.SO_REUSEADDR; +import static io.netty.channel.ChannelOption.SO_SNDBUF; +import static io.netty.channel.ChannelOption.TCP_NODELAY; + +public final class EpollSocketChannelConfig extends EpollChannelConfig implements SocketChannelConfig { + private volatile boolean allowHalfClosure; + private volatile boolean tcpFastopen; + + /** + * Creates a new instance. + */ + EpollSocketChannelConfig(EpollSocketChannel channel) { + super(channel); + + if (PlatformDependent.canEnableTcpNoDelayByDefault()) { + setTcpNoDelay(true); + } + calculateMaxBytesPerGatheringWrite(); + } + + @Override + public Map, Object> getOptions() { + return getOptions( + super.getOptions(), + SO_RCVBUF, SO_SNDBUF, TCP_NODELAY, SO_KEEPALIVE, SO_REUSEADDR, SO_LINGER, IP_TOS, + ALLOW_HALF_CLOSURE, EpollChannelOption.TCP_CORK, EpollChannelOption.TCP_NOTSENT_LOWAT, + EpollChannelOption.TCP_KEEPCNT, EpollChannelOption.TCP_KEEPIDLE, EpollChannelOption.TCP_KEEPINTVL, + EpollChannelOption.TCP_MD5SIG, EpollChannelOption.TCP_QUICKACK, EpollChannelOption.IP_TRANSPARENT, + ChannelOption.TCP_FASTOPEN_CONNECT, EpollChannelOption.SO_BUSY_POLL); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == SO_RCVBUF) { + return (T) Integer.valueOf(getReceiveBufferSize()); + } + if (option == SO_SNDBUF) { + return (T) Integer.valueOf(getSendBufferSize()); + } + if (option == TCP_NODELAY) { + return (T) Boolean.valueOf(isTcpNoDelay()); + } + if (option == SO_KEEPALIVE) { + return (T) Boolean.valueOf(isKeepAlive()); + } + if (option == SO_REUSEADDR) { + return (T) Boolean.valueOf(isReuseAddress()); + } + if (option == SO_LINGER) { + return (T) Integer.valueOf(getSoLinger()); + } + if (option == IP_TOS) { + return (T) Integer.valueOf(getTrafficClass()); + } + if (option == ALLOW_HALF_CLOSURE) { + return (T) Boolean.valueOf(isAllowHalfClosure()); + } + if (option == EpollChannelOption.TCP_CORK) { + return (T) Boolean.valueOf(isTcpCork()); + } + if (option == EpollChannelOption.TCP_NOTSENT_LOWAT) { + return (T) Long.valueOf(getTcpNotSentLowAt()); + } + if (option == EpollChannelOption.TCP_KEEPIDLE) { + return (T) Integer.valueOf(getTcpKeepIdle()); + } + if (option == EpollChannelOption.TCP_KEEPINTVL) { + return (T) Integer.valueOf(getTcpKeepIntvl()); + } + if (option == EpollChannelOption.TCP_KEEPCNT) { + return (T) Integer.valueOf(getTcpKeepCnt()); + } + if (option == EpollChannelOption.TCP_USER_TIMEOUT) { + return (T) Integer.valueOf(getTcpUserTimeout()); + } + if (option == EpollChannelOption.TCP_QUICKACK) { + return (T) Boolean.valueOf(isTcpQuickAck()); + } + if (option == EpollChannelOption.IP_TRANSPARENT) { + return (T) Boolean.valueOf(isIpTransparent()); + } + if (option == ChannelOption.TCP_FASTOPEN_CONNECT) { + return (T) Boolean.valueOf(isTcpFastOpenConnect()); + } + if (option == EpollChannelOption.SO_BUSY_POLL) { + return (T) Integer.valueOf(getSoBusyPoll()); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == SO_RCVBUF) { + setReceiveBufferSize((Integer) value); + } else if (option == SO_SNDBUF) { + setSendBufferSize((Integer) value); + } else if (option == TCP_NODELAY) { + setTcpNoDelay((Boolean) value); + } else if (option == SO_KEEPALIVE) { + setKeepAlive((Boolean) value); + } else if (option == SO_REUSEADDR) { + setReuseAddress((Boolean) value); + } else if (option == SO_LINGER) { + setSoLinger((Integer) value); + } else if (option == IP_TOS) { + setTrafficClass((Integer) value); + } else if (option == ALLOW_HALF_CLOSURE) { + setAllowHalfClosure((Boolean) value); + } else if (option == EpollChannelOption.TCP_CORK) { + setTcpCork((Boolean) value); + } else if (option == EpollChannelOption.TCP_NOTSENT_LOWAT) { + setTcpNotSentLowAt((Long) value); + } else if (option == EpollChannelOption.TCP_KEEPIDLE) { + setTcpKeepIdle((Integer) value); + } else if (option == EpollChannelOption.TCP_KEEPCNT) { + setTcpKeepCnt((Integer) value); + } else if (option == EpollChannelOption.TCP_KEEPINTVL) { + setTcpKeepIntvl((Integer) value); + } else if (option == EpollChannelOption.TCP_USER_TIMEOUT) { + setTcpUserTimeout((Integer) value); + } else if (option == EpollChannelOption.IP_TRANSPARENT) { + setIpTransparent((Boolean) value); + } else if (option == EpollChannelOption.TCP_MD5SIG) { + @SuppressWarnings("unchecked") + final Map m = (Map) value; + setTcpMd5Sig(m); + } else if (option == EpollChannelOption.TCP_QUICKACK) { + setTcpQuickAck((Boolean) value); + } else if (option == ChannelOption.TCP_FASTOPEN_CONNECT) { + setTcpFastOpenConnect((Boolean) value); + } else if (option == EpollChannelOption.SO_BUSY_POLL) { + setSoBusyPoll((Integer) value); + } else { + return super.setOption(option, value); + } + + return true; + } + + @Override + public int getReceiveBufferSize() { + try { + return ((EpollSocketChannel) channel).socket.getReceiveBufferSize(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getSendBufferSize() { + try { + return ((EpollSocketChannel) channel).socket.getSendBufferSize(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getSoLinger() { + try { + return ((EpollSocketChannel) channel).socket.getSoLinger(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public int getTrafficClass() { + try { + return ((EpollSocketChannel) channel).socket.getTrafficClass(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public boolean isKeepAlive() { + try { + return ((EpollSocketChannel) channel).socket.isKeepAlive(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public boolean isReuseAddress() { + try { + return ((EpollSocketChannel) channel).socket.isReuseAddress(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public boolean isTcpNoDelay() { + try { + return ((EpollSocketChannel) channel).socket.isTcpNoDelay(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Get the {@code TCP_CORK} option on the socket. See {@code man 7 tcp} for more details. + */ + public boolean isTcpCork() { + try { + return ((EpollSocketChannel) channel).socket.isTcpCork(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Get the {@code SO_BUSY_POLL} option on the socket. See {@code man 7 tcp} for more details. + */ + public int getSoBusyPoll() { + try { + return ((EpollSocketChannel) channel).socket.getSoBusyPoll(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Get the {@code TCP_NOTSENT_LOWAT} option on the socket. See {@code man 7 tcp} for more details. + * @return value is a uint32_t + */ + public long getTcpNotSentLowAt() { + try { + return ((EpollSocketChannel) channel).socket.getTcpNotSentLowAt(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Get the {@code TCP_KEEPIDLE} option on the socket. See {@code man 7 tcp} for more details. + */ + public int getTcpKeepIdle() { + try { + return ((EpollSocketChannel) channel).socket.getTcpKeepIdle(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Get the {@code TCP_KEEPINTVL} option on the socket. See {@code man 7 tcp} for more details. + */ + public int getTcpKeepIntvl() { + try { + return ((EpollSocketChannel) channel).socket.getTcpKeepIntvl(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Get the {@code TCP_KEEPCNT} option on the socket. See {@code man 7 tcp} for more details. + */ + public int getTcpKeepCnt() { + try { + return ((EpollSocketChannel) channel).socket.getTcpKeepCnt(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Get the {@code TCP_USER_TIMEOUT} option on the socket. See {@code man 7 tcp} for more details. + */ + public int getTcpUserTimeout() { + try { + return ((EpollSocketChannel) channel).socket.getTcpUserTimeout(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollSocketChannelConfig setKeepAlive(boolean keepAlive) { + try { + ((EpollSocketChannel) channel).socket.setKeepAlive(keepAlive); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollSocketChannelConfig setPerformancePreferences( + int connectionTime, int latency, int bandwidth) { + return this; + } + + @Override + public EpollSocketChannelConfig setReceiveBufferSize(int receiveBufferSize) { + try { + ((EpollSocketChannel) channel).socket.setReceiveBufferSize(receiveBufferSize); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollSocketChannelConfig setReuseAddress(boolean reuseAddress) { + try { + ((EpollSocketChannel) channel).socket.setReuseAddress(reuseAddress); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollSocketChannelConfig setSendBufferSize(int sendBufferSize) { + try { + ((EpollSocketChannel) channel).socket.setSendBufferSize(sendBufferSize); + calculateMaxBytesPerGatheringWrite(); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollSocketChannelConfig setSoLinger(int soLinger) { + try { + ((EpollSocketChannel) channel).socket.setSoLinger(soLinger); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollSocketChannelConfig setTcpNoDelay(boolean tcpNoDelay) { + try { + ((EpollSocketChannel) channel).socket.setTcpNoDelay(tcpNoDelay); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_CORK} option on the socket. See {@code man 7 tcp} for more details. + */ + public EpollSocketChannelConfig setTcpCork(boolean tcpCork) { + try { + ((EpollSocketChannel) channel).socket.setTcpCork(tcpCork); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code SO_BUSY_POLL} option on the socket. See {@code man 7 tcp} for more details. + */ + public EpollSocketChannelConfig setSoBusyPoll(int loopMicros) { + try { + ((EpollSocketChannel) channel).socket.setSoBusyPoll(loopMicros); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_NOTSENT_LOWAT} option on the socket. See {@code man 7 tcp} for more details. + * @param tcpNotSentLowAt is a uint32_t + */ + public EpollSocketChannelConfig setTcpNotSentLowAt(long tcpNotSentLowAt) { + try { + ((EpollSocketChannel) channel).socket.setTcpNotSentLowAt(tcpNotSentLowAt); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + @Override + public EpollSocketChannelConfig setTrafficClass(int trafficClass) { + try { + ((EpollSocketChannel) channel).socket.setTrafficClass(trafficClass); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_KEEPIDLE} option on the socket. See {@code man 7 tcp} for more details. + */ + public EpollSocketChannelConfig setTcpKeepIdle(int seconds) { + try { + ((EpollSocketChannel) channel).socket.setTcpKeepIdle(seconds); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_KEEPINTVL} option on the socket. See {@code man 7 tcp} for more details. + */ + public EpollSocketChannelConfig setTcpKeepIntvl(int seconds) { + try { + ((EpollSocketChannel) channel).socket.setTcpKeepIntvl(seconds); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * @deprecated use {@link #setTcpKeepCnt(int)} + */ + @Deprecated + public EpollSocketChannelConfig setTcpKeepCntl(int probes) { + return setTcpKeepCnt(probes); + } + + /** + * Set the {@code TCP_KEEPCNT} option on the socket. See {@code man 7 tcp} for more details. + */ + public EpollSocketChannelConfig setTcpKeepCnt(int probes) { + try { + ((EpollSocketChannel) channel).socket.setTcpKeepCnt(probes); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_USER_TIMEOUT} option on the socket. See {@code man 7 tcp} for more details. + */ + public EpollSocketChannelConfig setTcpUserTimeout(int milliseconds) { + try { + ((EpollSocketChannel) channel).socket.setTcpUserTimeout(milliseconds); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if IP_TRANSPARENT is enabled, + * {@code false} otherwise. + */ + public boolean isIpTransparent() { + try { + return ((EpollSocketChannel) channel).socket.isIpTransparent(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * If {@code true} is used IP_TRANSPARENT is enabled, + * {@code false} for disable it. Default is disabled. + */ + public EpollSocketChannelConfig setIpTransparent(boolean transparent) { + try { + ((EpollSocketChannel) channel).socket.setIpTransparent(transparent); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_MD5SIG} option on the socket. See {@code linux/tcp.h} for more details. + * Keys can only be set on, not read to prevent a potential leak, as they are confidential. + * Allowing them being read would mean anyone with access to the channel could get them. + */ + public EpollSocketChannelConfig setTcpMd5Sig(Map keys) { + try { + ((EpollSocketChannel) channel).setTcpMd5Sig(keys); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Set the {@code TCP_QUICKACK} option on the socket. + * See TCP_QUICKACK + * for more details. + */ + public EpollSocketChannelConfig setTcpQuickAck(boolean quickAck) { + try { + ((EpollSocketChannel) channel).socket.setTcpQuickAck(quickAck); + return this; + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Returns {@code true} if TCP_QUICKACK is enabled, + * {@code false} otherwise. + */ + public boolean isTcpQuickAck() { + try { + return ((EpollSocketChannel) channel).socket.isTcpQuickAck(); + } catch (IOException e) { + throw new ChannelException(e); + } + } + + /** + * Enables client TCP fast open. {@code TCP_FASTOPEN_CONNECT} normally + * requires Linux kernel 4.11 or later, so instead we use the traditional fast open + * client socket mechanics that work with kernel 3.6 and later. See this + * LWN article for more info. + */ + public EpollSocketChannelConfig setTcpFastOpenConnect(boolean fastOpenConnect) { + tcpFastopen = fastOpenConnect; + return this; + } + + /** + * Returns {@code true} if TCP fast open is enabled, {@code false} otherwise. + */ + public boolean isTcpFastOpenConnect() { + return tcpFastopen; + } + + @Override + public boolean isAllowHalfClosure() { + return allowHalfClosure; + } + + @Override + public EpollSocketChannelConfig setAllowHalfClosure(boolean allowHalfClosure) { + this.allowHalfClosure = allowHalfClosure; + return this; + } + + @Override + public EpollSocketChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + @Deprecated + public EpollSocketChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public EpollSocketChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public EpollSocketChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public EpollSocketChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public EpollSocketChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public EpollSocketChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + @Deprecated + public EpollSocketChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + @Deprecated + public EpollSocketChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public EpollSocketChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public EpollSocketChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + @Override + public EpollSocketChannelConfig setEpollMode(EpollMode mode) { + super.setEpollMode(mode); + return this; + } + + private void calculateMaxBytesPerGatheringWrite() { + // Multiply by 2 to give some extra space in case the OS can process write data faster than we can provide. + int newSendBufferSize = getSendBufferSize() << 1; + if (newSendBufferSize > 0) { + setMaxBytesPerGatheringWrite(newSendBufferSize); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollTcpInfo.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollTcpInfo.java new file mode 100644 index 0000000..e6e4ab6 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/EpollTcpInfo.java @@ -0,0 +1,193 @@ +/* + * Copyright 2014 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.channel.epoll; + +/** + *

+ * struct tcp_info + * { + * __u8 tcpi_state; + * __u8 tcpi_ca_state; + * __u8 tcpi_retransmits; + * __u8 tcpi_probes; + * __u8 tcpi_backoff; + * __u8 tcpi_options; + * __u8 tcpi_snd_wscale : 4, tcpi_rcv_wscale : 4; + * + * __u32 tcpi_rto; + * __u32 tcpi_ato; + * __u32 tcpi_snd_mss; + * __u32 tcpi_rcv_mss; + * + * __u32 tcpi_unacked; + * __u32 tcpi_sacked; + * __u32 tcpi_lost; + * __u32 tcpi_retrans; + * __u32 tcpi_fackets; + * + * __u32 tcpi_last_data_sent; + * __u32 tcpi_last_ack_sent; + * __u32 tcpi_last_data_recv; + * __u32 tcpi_last_ack_recv; + * + * __u32 tcpi_pmtu; + * __u32 tcpi_rcv_ssthresh; + * __u32 tcpi_rtt; + * __u32 tcpi_rttvar; + * __u32 tcpi_snd_ssthresh; + * __u32 tcpi_snd_cwnd; + * __u32 tcpi_advmss; + * __u32 tcpi_reordering; + * + * __u32 tcpi_rcv_rtt; + * __u32 tcpi_rcv_space; + * + * __u32 tcpi_total_retrans; + * }; + *

+ */ +public final class EpollTcpInfo { + + final long[] info = new long[32]; + + public int state() { + return (int) info[0]; + } + + public int caState() { + return (int) info[1]; + } + + public int retransmits() { + return (int) info[2]; + } + + public int probes() { + return (int) info[3]; + } + + public int backoff() { + return (int) info[4]; + } + + public int options() { + return (int) info[5]; + } + + public int sndWscale() { + return (int) info[6]; + } + + public int rcvWscale() { + return (int) info[7]; + } + + public long rto() { + return info[8]; + } + + public long ato() { + return info[9]; + } + + public long sndMss() { + return info[10]; + } + + public long rcvMss() { + return info[11]; + } + + public long unacked() { + return info[12]; + } + + public long sacked() { + return info[13]; + } + + public long lost() { + return info[14]; + } + + public long retrans() { + return info[15]; + } + + public long fackets() { + return info[16]; + } + + public long lastDataSent() { + return info[17]; + } + + public long lastAckSent() { + return info[18]; + } + + public long lastDataRecv() { + return info[19]; + } + + public long lastAckRecv() { + return info[20]; + } + + public long pmtu() { + return info[21]; + } + + public long rcvSsthresh() { + return info[22]; + } + + public long rtt() { + return info[23]; + } + + public long rttvar() { + return info[24]; + } + + public long sndSsthresh() { + return info[25]; + } + + public long sndCwnd() { + return info[26]; + } + + public long advmss() { + return info[27]; + } + + public long reordering() { + return info[28]; + } + + public long rcvRtt() { + return info[29]; + } + + public long rcvSpace() { + return info[30]; + } + + public long totalRetrans() { + return info[31]; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java new file mode 100644 index 0000000..32ab46d --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java @@ -0,0 +1,484 @@ +/* + * Copyright 2016 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.channel.epoll; + +import io.netty.channel.ChannelException; +import io.netty.channel.DefaultFileRegion; +import io.netty.channel.unix.Errors; +import io.netty.channel.unix.NativeInetAddress; +import io.netty.channel.unix.PeerCredentials; +import io.netty.channel.unix.Socket; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SocketUtils; +import io.netty.util.internal.UnstableApi; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Inet6Address; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Enumeration; + +import static io.netty.channel.unix.Errors.ioResult; +import static io.netty.channel.unix.Errors.newIOException; + +/** + * A socket which provides access Linux native methods. + */ +@UnstableApi +public final class LinuxSocket extends Socket { + static final InetAddress INET6_ANY = unsafeInetAddrByName("::"); + private static final InetAddress INET_ANY = unsafeInetAddrByName("0.0.0.0"); + private static final long MAX_UINT32_T = 0xFFFFFFFFL; + + LinuxSocket(int fd) { + super(fd); + } + + InternetProtocolFamily family() { + return ipv6 ? InternetProtocolFamily.IPv6 : InternetProtocolFamily.IPv4; + } + + int sendmmsg(NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + return Native.sendmmsg(intValue(), ipv6, msgs, offset, len); + } + + int recvmmsg(NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + return Native.recvmmsg(intValue(), ipv6, msgs, offset, len); + } + + int recvmsg(NativeDatagramPacketArray.NativeDatagramPacket msg) throws IOException { + return Native.recvmsg(intValue(), ipv6, msg); + } + + void setTimeToLive(int ttl) throws IOException { + setTimeToLive(intValue(), ttl); + } + + void setInterface(InetAddress address) throws IOException { + final NativeInetAddress a = NativeInetAddress.newInstance(address); + setInterface(intValue(), ipv6, a.address(), a.scopeId(), interfaceIndex(address)); + } + + void setNetworkInterface(NetworkInterface netInterface) throws IOException { + InetAddress address = deriveInetAddress(netInterface, family() == InternetProtocolFamily.IPv6); + if (address.equals(family() == InternetProtocolFamily.IPv4 ? INET_ANY : INET6_ANY)) { + throw new IOException("NetworkInterface does not support " + family()); + } + final NativeInetAddress nativeAddress = NativeInetAddress.newInstance(address); + setInterface(intValue(), ipv6, nativeAddress.address(), nativeAddress.scopeId(), interfaceIndex(netInterface)); + } + + InetAddress getInterface() throws IOException { + NetworkInterface inf = getNetworkInterface(); + if (inf != null) { + Enumeration addresses = SocketUtils.addressesFromNetworkInterface(inf); + if (addresses.hasMoreElements()) { + return addresses.nextElement(); + } + } + return null; + } + + NetworkInterface getNetworkInterface() throws IOException { + int ret = getInterface(intValue(), ipv6); + if (ipv6) { + return PlatformDependent.javaVersion() >= 7 ? NetworkInterface.getByIndex(ret) : null; + } + InetAddress address = inetAddress(ret); + return address != null ? NetworkInterface.getByInetAddress(address) : null; + } + + private static InetAddress inetAddress(int value) { + byte[] var1 = { + (byte) (value >>> 24 & 255), + (byte) (value >>> 16 & 255), + (byte) (value >>> 8 & 255), + (byte) (value & 255) + }; + + try { + return InetAddress.getByAddress(var1); + } catch (UnknownHostException ignore) { + return null; + } + } + + void joinGroup(InetAddress group, NetworkInterface netInterface, InetAddress source) throws IOException { + final NativeInetAddress g = NativeInetAddress.newInstance(group); + final boolean isIpv6 = group instanceof Inet6Address; + final NativeInetAddress i = NativeInetAddress.newInstance(deriveInetAddress(netInterface, isIpv6)); + if (source != null) { + if (source.getClass() != group.getClass()) { + throw new IllegalArgumentException("Source address is different type to group"); + } + final NativeInetAddress s = NativeInetAddress.newInstance(source); + joinSsmGroup(intValue(), ipv6 && isIpv6, g.address(), i.address(), + g.scopeId(), interfaceIndex(netInterface), s.address()); + } else { + joinGroup(intValue(), ipv6 && isIpv6, g.address(), i.address(), g.scopeId(), interfaceIndex(netInterface)); + } + } + + void leaveGroup(InetAddress group, NetworkInterface netInterface, InetAddress source) throws IOException { + final NativeInetAddress g = NativeInetAddress.newInstance(group); + final boolean isIpv6 = group instanceof Inet6Address; + final NativeInetAddress i = NativeInetAddress.newInstance(deriveInetAddress(netInterface, isIpv6)); + if (source != null) { + if (source.getClass() != group.getClass()) { + throw new IllegalArgumentException("Source address is different type to group"); + } + final NativeInetAddress s = NativeInetAddress.newInstance(source); + leaveSsmGroup(intValue(), ipv6 && isIpv6, g.address(), i.address(), + g.scopeId(), interfaceIndex(netInterface), s.address()); + } else { + leaveGroup(intValue(), ipv6 && isIpv6, g.address(), i.address(), g.scopeId(), interfaceIndex(netInterface)); + } + } + + private static int interfaceIndex(NetworkInterface networkInterface) { + return PlatformDependent.javaVersion() >= 7 ? networkInterface.getIndex() : -1; + } + + private static int interfaceIndex(InetAddress address) throws IOException { + if (PlatformDependent.javaVersion() >= 7) { + NetworkInterface iface = NetworkInterface.getByInetAddress(address); + if (iface != null) { + return iface.getIndex(); + } + } + return -1; + } + + void setTcpDeferAccept(int deferAccept) throws IOException { + setTcpDeferAccept(intValue(), deferAccept); + } + + void setTcpQuickAck(boolean quickAck) throws IOException { + setTcpQuickAck(intValue(), quickAck ? 1 : 0); + } + + void setTcpCork(boolean tcpCork) throws IOException { + setTcpCork(intValue(), tcpCork ? 1 : 0); + } + + void setSoBusyPoll(int loopMicros) throws IOException { + setSoBusyPoll(intValue(), loopMicros); + } + + void setTcpNotSentLowAt(long tcpNotSentLowAt) throws IOException { + if (tcpNotSentLowAt < 0 || tcpNotSentLowAt > MAX_UINT32_T) { + throw new IllegalArgumentException("tcpNotSentLowAt must be a uint32_t"); + } + setTcpNotSentLowAt(intValue(), (int) tcpNotSentLowAt); + } + + void setTcpFastOpen(int tcpFastopenBacklog) throws IOException { + setTcpFastOpen(intValue(), tcpFastopenBacklog); + } + + void setTcpKeepIdle(int seconds) throws IOException { + setTcpKeepIdle(intValue(), seconds); + } + + void setTcpKeepIntvl(int seconds) throws IOException { + setTcpKeepIntvl(intValue(), seconds); + } + + void setTcpKeepCnt(int probes) throws IOException { + setTcpKeepCnt(intValue(), probes); + } + + void setTcpUserTimeout(int milliseconds) throws IOException { + setTcpUserTimeout(intValue(), milliseconds); + } + + void setIpFreeBind(boolean enabled) throws IOException { + setIpFreeBind(intValue(), enabled ? 1 : 0); + } + + void setIpTransparent(boolean enabled) throws IOException { + setIpTransparent(intValue(), enabled ? 1 : 0); + } + + void setIpRecvOrigDestAddr(boolean enabled) throws IOException { + setIpRecvOrigDestAddr(intValue(), enabled ? 1 : 0); + } + + int getTimeToLive() throws IOException { + return getTimeToLive(intValue()); + } + + void getTcpInfo(EpollTcpInfo info) throws IOException { + getTcpInfo(intValue(), info.info); + } + + void setTcpMd5Sig(InetAddress address, byte[] key) throws IOException { + final NativeInetAddress a = NativeInetAddress.newInstance(address); + setTcpMd5Sig(intValue(), ipv6, a.address(), a.scopeId(), key); + } + + boolean isTcpCork() throws IOException { + return isTcpCork(intValue()) != 0; + } + + int getSoBusyPoll() throws IOException { + return getSoBusyPoll(intValue()); + } + + int getTcpDeferAccept() throws IOException { + return getTcpDeferAccept(intValue()); + } + + boolean isTcpQuickAck() throws IOException { + return isTcpQuickAck(intValue()) != 0; + } + + long getTcpNotSentLowAt() throws IOException { + return getTcpNotSentLowAt(intValue()) & MAX_UINT32_T; + } + + int getTcpKeepIdle() throws IOException { + return getTcpKeepIdle(intValue()); + } + + int getTcpKeepIntvl() throws IOException { + return getTcpKeepIntvl(intValue()); + } + + int getTcpKeepCnt() throws IOException { + return getTcpKeepCnt(intValue()); + } + + int getTcpUserTimeout() throws IOException { + return getTcpUserTimeout(intValue()); + } + + boolean isIpFreeBind() throws IOException { + return isIpFreeBind(intValue()) != 0; + } + + boolean isIpTransparent() throws IOException { + return isIpTransparent(intValue()) != 0; + } + + boolean isIpRecvOrigDestAddr() throws IOException { + return isIpRecvOrigDestAddr(intValue()) != 0; + } + + PeerCredentials getPeerCredentials() throws IOException { + return getPeerCredentials(intValue()); + } + + boolean isLoopbackModeDisabled() throws IOException { + return getIpMulticastLoop(intValue(), ipv6) == 0; + } + + void setLoopbackModeDisabled(boolean loopbackModeDisabled) throws IOException { + setIpMulticastLoop(intValue(), ipv6, loopbackModeDisabled ? 0 : 1); + } + + boolean isUdpGro() throws IOException { + return isUdpGro(intValue()) != 0; + } + + void setUdpGro(boolean gro) throws IOException { + setUdpGro(intValue(), gro ? 1 : 0); + } + + long sendFile(DefaultFileRegion src, long baseOffset, long offset, long length) throws IOException { + // Open the file-region as it may be created via the lazy constructor. This is needed as we directly access + // the FileChannel field via JNI. + src.open(); + + long res = sendFile(intValue(), src, baseOffset, offset, length); + if (res >= 0) { + return res; + } + return ioResult("sendfile", (int) res); + } + + public void bindVSock(VSockAddress address) throws IOException { + int res = bindVSock(/*fd*/intValue(), address.getCid(), address.getPort()); + if (res < 0) { + throw newIOException("bindVSock", res); + } + } + + public boolean connectVSock(VSockAddress address) throws IOException { + int res = connectVSock(/*fd*/intValue(), address.getCid(), address.getPort()); + if (res < 0) { + return Errors.handleConnectErrno("connectVSock", res); + } + return true; + } + + public VSockAddress remoteVSockAddress() { + byte[] addr = remoteVSockAddress(/*fd*/intValue()); + if (addr == null) { + return null; + } + int cid = getIntAt(addr, 0); + int port = getIntAt(addr, 4); + return new VSockAddress(cid, port); + } + + public VSockAddress localVSockAddress() { + byte[] addr = localVSockAddress(/*fd*/intValue()); + if (addr == null) { + return null; + } + int cid = getIntAt(addr, 0); + int port = getIntAt(addr, 4); + return new VSockAddress(cid, port); + } + + private static int getIntAt(byte[] array, int startIndex) { + return array[startIndex] << 24 | (array[startIndex + 1] & 0xFF) << 16 + | (array[startIndex + 2] & 0xFF) << 8 | (array[startIndex + 3] & 0xFF); + } + + private static InetAddress deriveInetAddress(NetworkInterface netInterface, boolean ipv6) { + final InetAddress ipAny = ipv6 ? INET6_ANY : INET_ANY; + if (netInterface != null) { + final Enumeration ias = netInterface.getInetAddresses(); + while (ias.hasMoreElements()) { + final InetAddress ia = ias.nextElement(); + final boolean isV6 = ia instanceof Inet6Address; + if (isV6 == ipv6) { + return ia; + } + } + } + return ipAny; + } + + public static LinuxSocket newSocket(int fd) { + return new LinuxSocket(fd); + } + + public static LinuxSocket newVSockStream() { + return new LinuxSocket(newVSockStream0()); + } + + static int newVSockStream0() { + int res = newVSockStreamFd(); + if (res < 0) { + throw new ChannelException(newIOException("newVSockStream", res)); + } + return res; + } + + public static LinuxSocket newSocketStream(boolean ipv6) { + return new LinuxSocket(newSocketStream0(ipv6)); + } + + public static LinuxSocket newSocketStream(InternetProtocolFamily protocol) { + return new LinuxSocket(newSocketStream0(protocol)); + } + + public static LinuxSocket newSocketStream() { + return newSocketStream(isIPv6Preferred()); + } + + public static LinuxSocket newSocketDgram(boolean ipv6) { + return new LinuxSocket(newSocketDgram0(ipv6)); + } + + public static LinuxSocket newSocketDgram(InternetProtocolFamily family) { + return new LinuxSocket(newSocketDgram0(family)); + } + + public static LinuxSocket newSocketDgram() { + return newSocketDgram(isIPv6Preferred()); + } + + public static LinuxSocket newSocketDomain() { + return new LinuxSocket(newSocketDomain0()); + } + + public static LinuxSocket newSocketDomainDgram() { + return new LinuxSocket(newSocketDomainDgram0()); + } + + private static InetAddress unsafeInetAddrByName(String inetName) { + try { + return InetAddress.getByName(inetName); + } catch (UnknownHostException uhe) { + throw new ChannelException(uhe); + } + } + + private static native int newVSockStreamFd(); + private static native int bindVSock(int fd, int cid, int port); + private static native int connectVSock(int fd, int cid, int port); + private static native byte[] remoteVSockAddress(int fd); + private static native byte[] localVSockAddress(int fd); + + private static native void joinGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex) throws IOException; + private static native void joinSsmGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex, byte[] source) throws IOException; + private static native void leaveGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex) throws IOException; + private static native void leaveSsmGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress, + int scopeId, int interfaceIndex, byte[] source) throws IOException; + private static native long sendFile(int socketFd, DefaultFileRegion src, long baseOffset, + long offset, long length) throws IOException; + + private static native int getTcpDeferAccept(int fd) throws IOException; + private static native int isTcpQuickAck(int fd) throws IOException; + private static native int isTcpCork(int fd) throws IOException; + private static native int getSoBusyPoll(int fd) throws IOException; + private static native int getTcpNotSentLowAt(int fd) throws IOException; + private static native int getTcpKeepIdle(int fd) throws IOException; + private static native int getTcpKeepIntvl(int fd) throws IOException; + private static native int getTcpKeepCnt(int fd) throws IOException; + private static native int getTcpUserTimeout(int fd) throws IOException; + private static native int getTimeToLive(int fd) throws IOException; + private static native int isIpFreeBind(int fd) throws IOException; + private static native int isIpTransparent(int fd) throws IOException; + private static native int isIpRecvOrigDestAddr(int fd) throws IOException; + private static native void getTcpInfo(int fd, long[] array) throws IOException; + private static native PeerCredentials getPeerCredentials(int fd) throws IOException; + + private static native void setTcpDeferAccept(int fd, int deferAccept) throws IOException; + private static native void setTcpQuickAck(int fd, int quickAck) throws IOException; + private static native void setTcpCork(int fd, int tcpCork) throws IOException; + private static native void setSoBusyPoll(int fd, int loopMicros) throws IOException; + private static native void setTcpNotSentLowAt(int fd, int tcpNotSentLowAt) throws IOException; + private static native void setTcpFastOpen(int fd, int tcpFastopenBacklog) throws IOException; + private static native void setTcpKeepIdle(int fd, int seconds) throws IOException; + private static native void setTcpKeepIntvl(int fd, int seconds) throws IOException; + private static native void setTcpKeepCnt(int fd, int probes) throws IOException; + private static native void setTcpUserTimeout(int fd, int milliseconds)throws IOException; + private static native void setIpFreeBind(int fd, int freeBind) throws IOException; + private static native void setIpTransparent(int fd, int transparent) throws IOException; + private static native void setIpRecvOrigDestAddr(int fd, int transparent) throws IOException; + private static native void setTcpMd5Sig( + int fd, boolean ipv6, byte[] address, int scopeId, byte[] key) throws IOException; + private static native void setInterface( + int fd, boolean ipv6, byte[] interfaceAddress, int scopeId, int networkInterfaceIndex) throws IOException; + private static native int getInterface(int fd, boolean ipv6); + private static native int getIpMulticastLoop(int fd, boolean ipv6) throws IOException; + private static native void setIpMulticastLoop(int fd, boolean ipv6, int enabled) throws IOException; + private static native void setTimeToLive(int fd, int ttl) throws IOException; + private static native int isUdpGro(int fd) throws IOException; + private static native void setUdpGro(int fd, int gro) throws IOException; +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/Native.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/Native.java new file mode 100644 index 0000000..85829ec --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/Native.java @@ -0,0 +1,338 @@ +/* + * Copyright 2013 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.channel.epoll; + +import io.netty.channel.DefaultFileRegion; +import io.netty.channel.unix.FileDescriptor; +import io.netty.channel.unix.PeerCredentials; +import io.netty.channel.unix.Socket; +import io.netty.channel.unix.Unix; +import io.netty.util.internal.ClassInitializerUtil; +import io.netty.util.internal.NativeLibraryLoader; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.Selector; + +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollerr; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollet; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollin; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollout; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollrdhup; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingRecvmmsg; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingSendmmsg; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.kernelVersion; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.tcpFastopenMode; +import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.tcpMd5SigMaxKeyLen; +import static io.netty.channel.unix.Errors.ioResult; +import static io.netty.channel.unix.Errors.newIOException; + +/** + * Native helper methods + *

Internal usage only! + *

Static members which call JNI methods must be defined in {@link NativeStaticallyReferencedJniMethods}. + */ +public final class Native { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(Native.class); + + static { + Selector selector = null; + try { + // We call Selector.open() as this will under the hood cause IOUtil to be loaded. + // This is a workaround for a possible classloader deadlock that could happen otherwise: + // + // See https://github.com/netty/netty/issues/10187 + selector = Selector.open(); + } catch (IOException ignore) { + // Just ignore + } + + // Preload all classes that will be used in the OnLoad(...) function of JNI to eliminate the possiblity of a + // class-loader deadlock. This is a workaround for https://github.com/netty/netty/issues/11209. + + // This needs to match all the classes that are loaded via NETTY_JNI_UTIL_LOAD_CLASS or looked up via + // NETTY_JNI_UTIL_FIND_CLASS. + ClassInitializerUtil.tryLoadClasses(Native.class, + // netty_epoll_linuxsocket + PeerCredentials.class, DefaultFileRegion.class, FileChannel.class, java.io.FileDescriptor.class, + // netty_epoll_native + NativeDatagramPacketArray.NativeDatagramPacket.class + ); + + try { + // First, try calling a side-effect free JNI method to see if the library was already + // loaded by the application. + offsetofEpollData(); + } catch (UnsatisfiedLinkError ignore) { + // The library was not previously loaded, load it now. + loadNativeLibrary(); + } finally { + try { + if (selector != null) { + selector.close(); + } + } catch (IOException ignore) { + // Just ignore + } + } + Unix.registerInternal(new Runnable() { + @Override + public void run() { + registerUnix(); + } + }); + } + + private static native int registerUnix(); + + // EventLoop operations and constants + public static final int EPOLLIN = epollin(); + public static final int EPOLLOUT = epollout(); + public static final int EPOLLRDHUP = epollrdhup(); + public static final int EPOLLET = epollet(); + public static final int EPOLLERR = epollerr(); + + public static final boolean IS_SUPPORTING_SENDMMSG = isSupportingSendmmsg(); + static final boolean IS_SUPPORTING_RECVMMSG = isSupportingRecvmmsg(); + static final boolean IS_SUPPORTING_UDP_SEGMENT = isSupportingUdpSegment(); + private static final int TFO_ENABLED_CLIENT_MASK = 0x1; + private static final int TFO_ENABLED_SERVER_MASK = 0x2; + private static final int TCP_FASTOPEN_MODE = tcpFastopenMode(); + /** + * tcp_fastopen client mode enabled + * state. + */ + static final boolean IS_SUPPORTING_TCP_FASTOPEN_CLIENT = + (TCP_FASTOPEN_MODE & TFO_ENABLED_CLIENT_MASK) == TFO_ENABLED_CLIENT_MASK; + /** + * tcp_fastopen server mode enabled + * state. + */ + static final boolean IS_SUPPORTING_TCP_FASTOPEN_SERVER = + (TCP_FASTOPEN_MODE & TFO_ENABLED_SERVER_MASK) == TFO_ENABLED_SERVER_MASK; + /** + * @deprecated Use {@link Epoll#isTcpFastOpenClientSideAvailable()} + * or {@link Epoll#isTcpFastOpenServerSideAvailable()}. + */ + @Deprecated + public static final boolean IS_SUPPORTING_TCP_FASTOPEN = IS_SUPPORTING_TCP_FASTOPEN_CLIENT || + IS_SUPPORTING_TCP_FASTOPEN_SERVER; + public static final int TCP_MD5SIG_MAXKEYLEN = tcpMd5SigMaxKeyLen(); + public static final String KERNEL_VERSION = kernelVersion(); + + public static FileDescriptor newEventFd() { + return new FileDescriptor(eventFd()); + } + + public static FileDescriptor newTimerFd() { + return new FileDescriptor(timerFd()); + } + + private static native boolean isSupportingUdpSegment(); + private static native int eventFd(); + private static native int timerFd(); + public static native void eventFdWrite(int fd, long value); + public static native void eventFdRead(int fd); + + public static FileDescriptor newEpollCreate() { + return new FileDescriptor(epollCreate()); + } + + private static native int epollCreate(); + + /** + * @deprecated this method is no longer supported. This functionality is internal to this package. + */ + @Deprecated + public static int epollWait(FileDescriptor epollFd, EpollEventArray events, FileDescriptor timerFd, + int timeoutSec, int timeoutNs) throws IOException { + long result = epollWait(epollFd, events, timerFd, timeoutSec, timeoutNs, -1); + return epollReady(result); + } + + static long epollWait(FileDescriptor epollFd, EpollEventArray events, FileDescriptor timerFd, + int timeoutSec, int timeoutNs, long millisThreshold) throws IOException { + if (timeoutSec == 0 && timeoutNs == 0) { + // Zero timeout => poll (aka return immediately) + // We shift this to be consistent with what is done in epollWait0(...) + return ((long) epollWait(epollFd, events, 0)) << 32; + } + if (timeoutSec == Integer.MAX_VALUE) { + // Max timeout => wait indefinitely: disarm timerfd first + timeoutSec = 0; + timeoutNs = 0; + } + long result = epollWait0(epollFd.intValue(), events.memoryAddress(), events.length(), timerFd.intValue(), + timeoutSec, timeoutNs, millisThreshold); + int ready = epollReady(result); + if (ready < 0) { + throw newIOException("epoll_wait", ready); + } + return result; + } + + // IMPORTANT: This needs to be consistent with what is used in netty_epoll_native.c + static int epollReady(long result) { + return (int) (result >> 32); + } + + // IMPORTANT: This needs to be consistent with what is used in netty_epoll_native.c + static boolean epollTimerWasUsed(long result) { + return (result & 0xff) != 0; + } + + static int epollWait(FileDescriptor epollFd, EpollEventArray events, boolean immediatePoll) throws IOException { + return epollWait(epollFd, events, immediatePoll ? 0 : -1); + } + + /** + * This uses epoll's own timeout and does not reset/re-arm any timerfd + */ + static int epollWait(FileDescriptor epollFd, EpollEventArray events, int timeoutMillis) throws IOException { + int ready = epollWait(epollFd.intValue(), events.memoryAddress(), events.length(), timeoutMillis); + if (ready < 0) { + throw newIOException("epoll_wait", ready); + } + return ready; + } + + /** + * Non-blocking variant of + * {@link #epollWait(FileDescriptor, EpollEventArray, FileDescriptor, int, int)} + * that will also hint to processor we are in a busy-wait loop. + */ + public static int epollBusyWait(FileDescriptor epollFd, EpollEventArray events) throws IOException { + int ready = epollBusyWait0(epollFd.intValue(), events.memoryAddress(), events.length()); + if (ready < 0) { + throw newIOException("epoll_wait", ready); + } + return ready; + } + + private static native long epollWait0( + int efd, long address, int len, int timerFd, int timeoutSec, int timeoutNs, long millisThreshold); + private static native int epollWait(int efd, long address, int len, int timeout); + private static native int epollBusyWait0(int efd, long address, int len); + + public static void epollCtlAdd(int efd, final int fd, final int flags) throws IOException { + int res = epollCtlAdd0(efd, fd, flags); + if (res < 0) { + throw newIOException("epoll_ctl", res); + } + } + private static native int epollCtlAdd0(int efd, int fd, int flags); + + public static void epollCtlMod(int efd, final int fd, final int flags) throws IOException { + int res = epollCtlMod0(efd, fd, flags); + if (res < 0) { + throw newIOException("epoll_ctl", res); + } + } + private static native int epollCtlMod0(int efd, int fd, int flags); + + public static void epollCtlDel(int efd, final int fd) throws IOException { + int res = epollCtlDel0(efd, fd); + if (res < 0) { + throw newIOException("epoll_ctl", res); + } + } + private static native int epollCtlDel0(int efd, int fd); + + // File-descriptor operations + public static int splice(int fd, long offIn, int fdOut, long offOut, long len) throws IOException { + int res = splice0(fd, offIn, fdOut, offOut, len); + if (res >= 0) { + return res; + } + return ioResult("splice", res); + } + + private static native int splice0(int fd, long offIn, int fdOut, long offOut, long len); + + @Deprecated + public static int sendmmsg(int fd, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + return sendmmsg(fd, Socket.isIPv6Preferred(), msgs, offset, len); + } + + static int sendmmsg(int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + int res = sendmmsg0(fd, ipv6, msgs, offset, len); + if (res >= 0) { + return res; + } + return ioResult("sendmmsg", res); + } + + private static native int sendmmsg0( + int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len); + + static int recvmmsg(int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, + int offset, int len) throws IOException { + int res = recvmmsg0(fd, ipv6, msgs, offset, len); + if (res >= 0) { + return res; + } + return ioResult("recvmmsg", res); + } + + private static native int recvmmsg0( + int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len); + + static int recvmsg(int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket packet) throws IOException { + int res = recvmsg0(fd, ipv6, packet); + if (res >= 0) { + return res; + } + return ioResult("recvmsg", res); + } + + private static native int recvmsg0( + int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket msg); + + // epoll_event related + public static native int sizeofEpollEvent(); + public static native int offsetofEpollData(); + + private static void loadNativeLibrary() { + String name = PlatformDependent.normalizedOs(); + if (!"linux".equals(name)) { + throw new IllegalStateException("Only supported on Linux"); + } + String staticLibName = "netty_transport_native_epoll"; + String sharedLibName = staticLibName + '_' + PlatformDependent.normalizedArch(); + ClassLoader cl = PlatformDependent.getClassLoader(Native.class); + try { + NativeLibraryLoader.load(sharedLibName, cl); + } catch (UnsatisfiedLinkError e1) { + try { + NativeLibraryLoader.load(staticLibName, cl); + logger.debug("Failed to load {}", sharedLibName, e1); + } catch (UnsatisfiedLinkError e2) { + ThrowableUtil.addSuppressed(e1, e2); + throw e1; + } + } + } + + private Native() { + // utility + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java new file mode 100644 index 0000000..b076559 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java @@ -0,0 +1,232 @@ +/* + * Copyright 2014 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.channel.epoll; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelOutboundBuffer.MessageProcessor; +import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.unix.IovArray; +import io.netty.channel.unix.Limits; +import io.netty.channel.unix.SegmentedDatagramPacket; +import io.netty.util.internal.UnstableApi; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import static io.netty.channel.unix.Limits.UIO_MAX_IOV; +import static io.netty.channel.unix.NativeInetAddress.copyIpv4MappedIpv6Address; + +/** + * Support sendmmsg(...) on linux with GLIBC 2.14+ + */ +final class NativeDatagramPacketArray { + + // Use UIO_MAX_IOV as this is the maximum number we can write with one sendmmsg(...) call. + private final NativeDatagramPacket[] packets = new NativeDatagramPacket[UIO_MAX_IOV]; + + // We share one IovArray for all NativeDatagramPackets to reduce memory overhead. This will allow us to write + // up to IOV_MAX iovec across all messages in one sendmmsg(...) call. + private final IovArray iovArray = new IovArray(); + + // temporary array to copy the ipv4 part of ipv6-mapped-ipv4 addresses and then create a Inet4Address out of it. + private final byte[] ipv4Bytes = new byte[4]; + private final MyMessageProcessor processor = new MyMessageProcessor(); + + private int count; + + NativeDatagramPacketArray() { + for (int i = 0; i < packets.length; i++) { + packets[i] = new NativeDatagramPacket(); + } + } + + boolean addWritable(ByteBuf buf, int index, int len) { + return add0(buf, index, len, 0, null); + } + + private boolean add0(ByteBuf buf, int index, int len, int segmentLen, InetSocketAddress recipient) { + if (count == packets.length) { + // We already filled up to UIO_MAX_IOV messages. This is the max allowed per + // recvmmsg(...) / sendmmsg(...) call, we will try again later. + return false; + } + if (len == 0) { + return true; + } + int offset = iovArray.count(); + if (offset == Limits.IOV_MAX || !iovArray.add(buf, index, len)) { + // Not enough space to hold the whole content, we will try again later. + return false; + } + NativeDatagramPacket p = packets[count]; + p.init(iovArray.memoryAddress(offset), iovArray.count() - offset, segmentLen, recipient); + + count++; + return true; + } + + void add(ChannelOutboundBuffer buffer, boolean connected, int maxMessagesPerWrite) throws Exception { + processor.connected = connected; + processor.maxMessagesPerWrite = maxMessagesPerWrite; + buffer.forEachFlushedMessage(processor); + } + + /** + * Returns the count + */ + int count() { + return count; + } + + /** + * Returns an array with {@link #count()} {@link NativeDatagramPacket}s filled. + */ + NativeDatagramPacket[] packets() { + return packets; + } + + void clear() { + this.count = 0; + this.iovArray.clear(); + } + + void release() { + iovArray.release(); + } + + private final class MyMessageProcessor implements MessageProcessor { + private boolean connected; + private int maxMessagesPerWrite; + + @Override + public boolean processMessage(Object msg) { + final boolean added; + if (msg instanceof DatagramPacket) { + DatagramPacket packet = (DatagramPacket) msg; + ByteBuf buf = packet.content(); + int segmentSize = 0; + if (packet instanceof io.netty.channel.unix.SegmentedDatagramPacket) { + int seg = ((io.netty.channel.unix.SegmentedDatagramPacket) packet).segmentSize(); + // We only need to tell the kernel that we want to use UDP_SEGMENT if there are multiple + // segments in the packet. + if (buf.readableBytes() > seg) { + segmentSize = seg; + } + } + added = add0(buf, buf.readerIndex(), buf.readableBytes(), segmentSize, packet.recipient()); + } else if (msg instanceof ByteBuf && connected) { + ByteBuf buf = (ByteBuf) msg; + added = add0(buf, buf.readerIndex(), buf.readableBytes(), 0, null); + } else { + added = false; + } + if (added) { + maxMessagesPerWrite--; + return maxMessagesPerWrite > 0; + } + return false; + } + } + + private static InetSocketAddress newAddress(byte[] addr, int addrLen, int port, int scopeId, byte[] ipv4Bytes) + throws UnknownHostException { + final InetAddress address; + if (addrLen == ipv4Bytes.length) { + System.arraycopy(addr, 0, ipv4Bytes, 0, addrLen); + address = InetAddress.getByAddress(ipv4Bytes); + } else { + address = Inet6Address.getByAddress(null, addr, scopeId); + } + return new InetSocketAddress(address, port); + } + + /** + * Used to pass needed data to JNI. + */ + @SuppressWarnings("unused") + @UnstableApi + public final class NativeDatagramPacket { + + // IMPORTANT: Most of the below variables are accessed via JNI. Be aware if you change any of these you also + // need to change these in the related .c file! + + // This is the actual struct iovec* + private long memoryAddress; + private int count; + + private final byte[] senderAddr = new byte[16]; + private int senderAddrLen; + private int senderScopeId; + private int senderPort; + + private final byte[] recipientAddr = new byte[16]; + private int recipientAddrLen; + private int recipientScopeId; + private int recipientPort; + + private int segmentSize; + + private void init(long memoryAddress, int count, int segmentSize, InetSocketAddress recipient) { + this.memoryAddress = memoryAddress; + this.count = count; + this.segmentSize = segmentSize; + + this.senderScopeId = 0; + this.senderPort = 0; + this.senderAddrLen = 0; + + if (recipient == null) { + this.recipientScopeId = 0; + this.recipientPort = 0; + this.recipientAddrLen = 0; + } else { + InetAddress address = recipient.getAddress(); + if (address instanceof Inet6Address) { + System.arraycopy(address.getAddress(), 0, recipientAddr, 0, recipientAddr.length); + recipientScopeId = ((Inet6Address) address).getScopeId(); + } else { + copyIpv4MappedIpv6Address(address.getAddress(), recipientAddr); + recipientScopeId = 0; + } + recipientAddrLen = recipientAddr.length; + recipientPort = recipient.getPort(); + } + } + + boolean hasSender() { + return senderPort > 0; + } + + DatagramPacket newDatagramPacket(ByteBuf buffer, InetSocketAddress recipient) throws UnknownHostException { + InetSocketAddress sender = newAddress(senderAddr, senderAddrLen, senderPort, senderScopeId, ipv4Bytes); + if (recipientAddrLen != 0) { + recipient = newAddress(recipientAddr, recipientAddrLen, recipientPort, recipientScopeId, ipv4Bytes); + } + + // Slice out the buffer with the correct length. + ByteBuf slice = buffer.retainedSlice(buffer.readerIndex(), count); + + // UDP_GRO + if (segmentSize > 0) { + return new SegmentedDatagramPacket(slice, segmentSize, recipient, sender); + } + return new DatagramPacket(slice, recipient, sender); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java new file mode 100644 index 0000000..b52721b --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 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.channel.epoll; + +/** + * This class is necessary to break the following cyclic dependency: + *

    + *
  1. JNI_OnLoad
  2. + *
  3. JNI Calls FindClass because RegisterNatives (used to register JNI methods) requires a class
  4. + *
  5. FindClass loads the class, but static members variables of that class attempt to call a JNI method which has not + * yet been registered.
  6. + *
  7. java.lang.UnsatisfiedLinkError is thrown because native method has not yet been registered.
  8. + *
+ * Static members which call JNI methods must not be declared in this class! + */ +final class NativeStaticallyReferencedJniMethods { + + private NativeStaticallyReferencedJniMethods() { } + + static native int epollin(); + static native int epollout(); + static native int epollrdhup(); + static native int epollet(); + static native int epollerr(); + static native long ssizeMax(); + static native int tcpMd5SigMaxKeyLen(); + static native int iovMax(); + static native int uioMaxIov(); + static native boolean isSupportingSendmmsg(); + static native boolean isSupportingRecvmmsg(); + static native int tcpFastopenMode(); + static native String kernelVersion(); +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/SegmentedDatagramPacket.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/SegmentedDatagramPacket.java new file mode 100644 index 0000000..50bca1f --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/SegmentedDatagramPacket.java @@ -0,0 +1,111 @@ +/* + * 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.channel.epoll; + +import io.netty.buffer.ByteBuf; + +import java.net.InetSocketAddress; + +/** + * @deprecated use {@link io.netty.channel.unix.SegmentedDatagramPacket}. + */ +@Deprecated +public final class SegmentedDatagramPacket extends io.netty.channel.unix.SegmentedDatagramPacket { + + /** + * Create a new instance. + * + * @param data the {@link ByteBuf} which must be continguous. + * @param segmentSize the segment size. + * @param recipient the recipient. + */ + public SegmentedDatagramPacket(ByteBuf data, int segmentSize, InetSocketAddress recipient) { + super(data, segmentSize, recipient); + checkIsSupported(); + } + + /** + * Create a new instance. + * + * @param data the {@link ByteBuf} which must be continguous. + * @param segmentSize the segment size. + * @param recipient the recipient. + */ + public SegmentedDatagramPacket(ByteBuf data, int segmentSize, + InetSocketAddress recipient, InetSocketAddress sender) { + super(data, segmentSize, recipient, sender); + checkIsSupported(); + } + + /** + * Returns {@code true} if the underlying system supports GSO. + */ + public static boolean isSupported() { + return Epoll.isAvailable() && + // We only support it together with sendmmsg(...) + Native.IS_SUPPORTING_SENDMMSG && Native.IS_SUPPORTING_UDP_SEGMENT; + } + + @Override + public SegmentedDatagramPacket copy() { + return new SegmentedDatagramPacket(content().copy(), segmentSize(), recipient(), sender()); + } + + @Override + public SegmentedDatagramPacket duplicate() { + return new SegmentedDatagramPacket(content().duplicate(), segmentSize(), recipient(), sender()); + } + + @Override + public SegmentedDatagramPacket retainedDuplicate() { + return new SegmentedDatagramPacket(content().retainedDuplicate(), segmentSize(), recipient(), sender()); + } + + @Override + public SegmentedDatagramPacket replace(ByteBuf content) { + return new SegmentedDatagramPacket(content, segmentSize(), recipient(), sender()); + } + + @Override + public SegmentedDatagramPacket retain() { + super.retain(); + return this; + } + + @Override + public SegmentedDatagramPacket retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public SegmentedDatagramPacket touch() { + super.touch(); + return this; + } + + @Override + public SegmentedDatagramPacket touch(Object hint) { + super.touch(hint); + return this; + } + + private static void checkIsSupported() { + if (!isSupported()) { + throw new IllegalStateException(); + } + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java new file mode 100644 index 0000000..5f545db --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015 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.channel.epoll; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkNotNullWithIAE; +import static io.netty.util.internal.ObjectUtil.checkNonEmpty; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; + +final class TcpMd5Util { + + static Collection newTcpMd5Sigs(AbstractEpollChannel channel, Collection current, + Map newKeys) throws IOException { + checkNotNull(channel, "channel"); + checkNotNull(current, "current"); + checkNotNull(newKeys, "newKeys"); + + // Validate incoming values + for (Entry e : newKeys.entrySet()) { + final byte[] key = e.getValue(); + checkNotNullWithIAE(e.getKey(), "e.getKey"); + checkNonEmpty(key, e.getKey().toString()); + if (key.length > Native.TCP_MD5SIG_MAXKEYLEN) { + throw new IllegalArgumentException("newKeys[" + e.getKey() + + "] has a key with invalid length; should not exceed the maximum length (" + + Native.TCP_MD5SIG_MAXKEYLEN + ')'); + } + } + + // Remove mappings not present in the new set. + for (InetAddress addr : current) { + if (!newKeys.containsKey(addr)) { + channel.socket.setTcpMd5Sig(addr, null); + } + } + + if (newKeys.isEmpty()) { + return Collections.emptySet(); + } + + // Set new mappings and store addresses which we set. + final Collection addresses = new ArrayList(newKeys.size()); + for (Entry e : newKeys.entrySet()) { + channel.socket.setTcpMd5Sig(e.getKey(), e.getValue()); + addresses.add(e.getKey()); + } + + return addresses; + } + + private TcpMd5Util() { + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/VSockAddress.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/VSockAddress.java new file mode 100644 index 0000000..d410072 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/VSockAddress.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023 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.channel.epoll; + +import java.net.SocketAddress; + +/** + * A address for a + * VM sockets (Linux VSOCK address family). + */ + +public final class VSockAddress extends SocketAddress { + private static final long serialVersionUID = 8600894096347158429L; + + public static final int VMADDR_CID_ANY = -1; + public static final int VMADDR_CID_HYPERVISOR = 0; + public static final int VMADDR_CID_LOCAL = 1; + public static final int VMADDR_CID_HOST = 2; + + public static final int VMADDR_PORT_ANY = -1; + + private final int cid; + private final int port; + + public VSockAddress(int cid, int port) { + this.cid = cid; + this.port = port; + } + + public int getCid() { + return cid; + } + + public int getPort() { + return port; + } + + @Override + public String toString() { + return "VSockAddress{" + + "cid=" + cid + + ", port=" + port + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VSockAddress)) { + return false; + } + + VSockAddress that = (VSockAddress) o; + + return cid == that.cid && port == that.port; + } + + @Override + public int hashCode() { + int result = cid; + result = 31 * result + port; + return result; + } +} diff --git a/netty-channel-epoll/src/main/java/io/netty/channel/epoll/package-info.java b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/package-info.java new file mode 100644 index 0000000..7a96dc1 --- /dev/null +++ b/netty-channel-epoll/src/main/java/io/netty/channel/epoll/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014 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. + */ + +/** + * Optimized transport for linux which uses EPOLL Edge-Triggered Mode + * for maximal performance. + */ +package io.netty.channel.epoll; diff --git a/netty-channel-epoll/src/main/java/module-info.java b/netty-channel-epoll/src/main/java/module-info.java new file mode 100644 index 0000000..8ff489f --- /dev/null +++ b/netty-channel-epoll/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module org.xbib.io.netty.channel.epoll { + exports io.netty.channel.epoll; + requires org.xbib.io.netty.buffer; + requires org.xbib.io.netty.channel; + requires org.xbib.io.netty.channel.unix; + requires org.xbib.io.netty.util; +} diff --git a/netty-channel/src/main/java/module-info.java b/netty-channel/src/main/java/module-info.java index 2a47566..7faabd5 100644 --- a/netty-channel/src/main/java/module-info.java +++ b/netty-channel/src/main/java/module-info.java @@ -5,6 +5,7 @@ module org.xbib.io.netty.channel { exports io.netty.channel; exports io.netty.channel.embedded; exports io.netty.channel.group; + exports io.netty.channel.internal to org.xbib.io.netty.channel.epoll; exports io.netty.channel.local; exports io.netty.channel.nio; exports io.netty.channel.oio; diff --git a/netty-handler-codec-http3/build.gradle b/netty-handler-codec-http3/build.gradle new file mode 100644 index 0000000..bafe222 --- /dev/null +++ b/netty-handler-codec-http3/build.gradle @@ -0,0 +1,6 @@ +dependencies { + api project(':netty-handler-codec-http') + api project(':netty-handler-codec-quic') + testImplementation testLibs.assertj + testImplementation testLibs.mockito.core +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/CharSequenceMap.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/CharSequenceMap.java new file mode 100644 index 0000000..0809889 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/CharSequenceMap.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.handler.codec.DefaultHeaders; +import io.netty.handler.codec.UnsupportedValueConverter; +import io.netty.handler.codec.ValueConverter; + +import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER; +import static io.netty.util.AsciiString.CASE_SENSITIVE_HASHER; + +/** + * Internal use only! + */ +final class CharSequenceMap extends DefaultHeaders> { + CharSequenceMap() { + this(true); + } + + CharSequenceMap(boolean caseSensitive) { + this(caseSensitive, UnsupportedValueConverter.instance()); + } + + CharSequenceMap(boolean caseSensitive, ValueConverter valueConverter) { + super(caseSensitive ? CASE_SENSITIVE_HASHER : CASE_INSENSITIVE_HASHER, valueConverter); + } + + @SuppressWarnings("unchecked") + CharSequenceMap(boolean caseSensitive, ValueConverter valueConverter, int arraySizeHint) { + super(caseSensitive ? CASE_SENSITIVE_HASHER : CASE_INSENSITIVE_HASHER, valueConverter, + NameValidator.NOT_NULL, arraySizeHint); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3CancelPushFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3CancelPushFrame.java new file mode 100644 index 0000000..1e833d8 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3CancelPushFrame.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + +import java.util.Objects; + +public final class DefaultHttp3CancelPushFrame implements Http3CancelPushFrame { + private final long id; + + public DefaultHttp3CancelPushFrame(long id) { + this.id = ObjectUtil.checkPositiveOrZero(id, "id"); + } + + @Override + public long id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultHttp3CancelPushFrame that = (DefaultHttp3CancelPushFrame) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(id=" + id() + ')'; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3DataFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3DataFrame.java new file mode 100644 index 0000000..20e293a --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3DataFrame.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; +import io.netty.util.internal.StringUtil; + +public final class DefaultHttp3DataFrame extends DefaultByteBufHolder implements Http3DataFrame { + + public DefaultHttp3DataFrame(ByteBuf data) { + super(data); + } + + @Override + public Http3DataFrame copy() { + return new DefaultHttp3DataFrame(content().copy()); + } + + @Override + public Http3DataFrame duplicate() { + return new DefaultHttp3DataFrame(content().duplicate()); + } + + @Override + public Http3DataFrame retainedDuplicate() { + return new DefaultHttp3DataFrame(content().retainedDuplicate()); + } + + @Override + public Http3DataFrame replace(ByteBuf content) { + return new DefaultHttp3DataFrame(content); + } + + @Override + public Http3DataFrame retain() { + super.retain(); + return this; + } + + @Override + public Http3DataFrame retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public Http3DataFrame touch() { + super.touch(); + return this; + } + + @Override + public Http3DataFrame touch(Object hint) { + super.touch(hint); + return this; + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(content=" + content() + ')'; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3GoAwayFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3GoAwayFrame.java new file mode 100644 index 0000000..65a3fe2 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3GoAwayFrame.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + +import java.util.Objects; + +public final class DefaultHttp3GoAwayFrame implements Http3GoAwayFrame { + private final long id; + + public DefaultHttp3GoAwayFrame(long id) { + this.id = ObjectUtil.checkPositiveOrZero(id, "id"); + } + + @Override + public long id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultHttp3GoAwayFrame that = (DefaultHttp3GoAwayFrame) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(id=" + id() + ')'; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3Headers.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3Headers.java new file mode 100644 index 0000000..873ded4 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3Headers.java @@ -0,0 +1,225 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.handler.codec.CharSequenceValueConverter; +import io.netty.handler.codec.DefaultHeaders; +import io.netty.util.AsciiString; +import io.netty.util.ByteProcessor; + +import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.hasPseudoHeaderFormat; +import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER; +import static io.netty.util.AsciiString.CASE_SENSITIVE_HASHER; +import static io.netty.util.AsciiString.isUpperCase; + +public final class DefaultHttp3Headers + extends DefaultHeaders implements Http3Headers { + private static final ByteProcessor HTTP3_NAME_VALIDATOR_PROCESSOR = new ByteProcessor() { + @Override + public boolean process(byte value) { + return !isUpperCase(value); + } + }; + static final NameValidator HTTP3_NAME_VALIDATOR = new NameValidator() { + @Override + public void validateName(CharSequence name) { + if (name == null || name.length() == 0) { + throw new Http3HeadersValidationException(String.format("empty headers are not allowed [%s]", name)); + } + if (name instanceof AsciiString) { + final int index; + try { + index = ((AsciiString) name).forEachByte(HTTP3_NAME_VALIDATOR_PROCESSOR); + } catch (Http3HeadersValidationException e) { + throw e; + } catch (Throwable t) { + throw new Http3HeadersValidationException( + String.format("unexpected error. invalid header name [%s]", name), t); + } + + if (index != -1) { + throw new Http3HeadersValidationException(String.format("invalid header name [%s]", name)); + } + } else { + for (int i = 0; i < name.length(); ++i) { + if (isUpperCase(name.charAt(i))) { + throw new Http3HeadersValidationException(String.format("invalid header name [%s]", name)); + } + } + } + } + }; + + private HeaderEntry firstNonPseudo = head; + + /** + * Create a new instance. + *

+ * Header names will be validated according to + * rfc7540. + */ + public DefaultHttp3Headers() { + this(true); + } + + /** + * Create a new instance. + * @param validate {@code true} to validate header names according to + * rfc7540. {@code false} to not validate header names. + */ + @SuppressWarnings("unchecked") + public DefaultHttp3Headers(boolean validate) { + // Case sensitive compare is used because it is cheaper, and header validation can be used to catch invalid + // headers. + super(CASE_SENSITIVE_HASHER, + CharSequenceValueConverter.INSTANCE, + validate ? HTTP3_NAME_VALIDATOR : NameValidator.NOT_NULL); + } + + /** + * Create a new instance. + * @param validate {@code true} to validate header names according to + * rfc7540. {@code false} to not validate header names. + * @param arraySizeHint A hint as to how large the hash data structure should be. + * The next positive power of two will be used. An upper bound may be enforced. + */ + @SuppressWarnings("unchecked") + public DefaultHttp3Headers(boolean validate, int arraySizeHint) { + // Case sensitive compare is used because it is cheaper, and header validation can be used to catch invalid + // headers. + super(CASE_SENSITIVE_HASHER, + CharSequenceValueConverter.INSTANCE, + validate ? HTTP3_NAME_VALIDATOR : NameValidator.NOT_NULL, + arraySizeHint); + } + + @Override + public Http3Headers clear() { + this.firstNonPseudo = head; + return super.clear(); + } + + @Override + public boolean equals(Object o) { + return o instanceof Http3Headers && equals((Http3Headers) o, CASE_SENSITIVE_HASHER); + } + + @Override + public int hashCode() { + return hashCode(CASE_SENSITIVE_HASHER); + } + + @Override + public Http3Headers method(CharSequence value) { + set(PseudoHeaderName.METHOD.value(), value); + return this; + } + + @Override + public Http3Headers scheme(CharSequence value) { + set(PseudoHeaderName.SCHEME.value(), value); + return this; + } + + @Override + public Http3Headers authority(CharSequence value) { + set(PseudoHeaderName.AUTHORITY.value(), value); + return this; + } + + @Override + public Http3Headers path(CharSequence value) { + set(PseudoHeaderName.PATH.value(), value); + return this; + } + + @Override + public Http3Headers status(CharSequence value) { + set(PseudoHeaderName.STATUS.value(), value); + return this; + } + + @Override + public CharSequence method() { + return get(PseudoHeaderName.METHOD.value()); + } + + @Override + public CharSequence scheme() { + return get(PseudoHeaderName.SCHEME.value()); + } + + @Override + public CharSequence authority() { + return get(PseudoHeaderName.AUTHORITY.value()); + } + + @Override + public CharSequence path() { + return get(PseudoHeaderName.PATH.value()); + } + + @Override + public CharSequence status() { + return get(PseudoHeaderName.STATUS.value()); + } + + @Override + public boolean contains(CharSequence name, CharSequence value) { + return contains(name, value, false); + } + + @Override + public boolean contains(CharSequence name, CharSequence value, boolean caseInsensitive) { + return contains(name, value, caseInsensitive ? CASE_INSENSITIVE_HASHER : CASE_SENSITIVE_HASHER); + } + + @Override + protected HeaderEntry newHeaderEntry(int h, CharSequence name, CharSequence value, + HeaderEntry next) { + return new Http3HeaderEntry(h, name, value, next); + } + + private final class Http3HeaderEntry extends HeaderEntry { + protected Http3HeaderEntry(int hash, CharSequence key, CharSequence value, + HeaderEntry next) { + super(hash, key); + this.value = value; + this.next = next; + + // Make sure the pseudo headers fields are first in iteration order + if (hasPseudoHeaderFormat(key)) { + after = firstNonPseudo; + before = firstNonPseudo.before(); + } else { + after = head; + before = head.before(); + if (firstNonPseudo == head) { + firstNonPseudo = this; + } + } + pointNeighborsToThis(); + } + + @Override + protected void remove() { + if (this == firstNonPseudo) { + firstNonPseudo = firstNonPseudo.after(); + } + super.remove(); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3HeadersFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3HeadersFrame.java new file mode 100644 index 0000000..2ad3e5e --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3HeadersFrame.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + +import java.util.Objects; + +public final class DefaultHttp3HeadersFrame implements Http3HeadersFrame { + + private final Http3Headers headers; + + public DefaultHttp3HeadersFrame() { + this(new DefaultHttp3Headers()); + } + + public DefaultHttp3HeadersFrame(Http3Headers headers) { + this.headers = ObjectUtil.checkNotNull(headers, "headers"); + } + + @Override + public Http3Headers headers() { + return headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultHttp3HeadersFrame that = (DefaultHttp3HeadersFrame) o; + return Objects.equals(headers, that.headers); + } + + @Override + public int hashCode() { + return Objects.hash(headers); + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(headers=" + headers() + ')'; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3MaxPushIdFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3MaxPushIdFrame.java new file mode 100644 index 0000000..cdc96f3 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3MaxPushIdFrame.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + +import java.util.Objects; + +public final class DefaultHttp3MaxPushIdFrame implements Http3MaxPushIdFrame { + private final long id; + + public DefaultHttp3MaxPushIdFrame(long id) { + this.id = ObjectUtil.checkPositiveOrZero(id, "id"); + } + + @Override + public long id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultHttp3MaxPushIdFrame that = (DefaultHttp3MaxPushIdFrame) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(id=" + id() + ')'; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3PushPromiseFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3PushPromiseFrame.java new file mode 100644 index 0000000..39a61e3 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3PushPromiseFrame.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + +import java.util.Objects; + +public final class DefaultHttp3PushPromiseFrame implements Http3PushPromiseFrame { + + private final long id; + private final Http3Headers headers; + + public DefaultHttp3PushPromiseFrame(long id) { + this(id, new DefaultHttp3Headers()); + } + + public DefaultHttp3PushPromiseFrame(long id, Http3Headers headers) { + this.id = ObjectUtil.checkPositiveOrZero(id, "id"); + this.headers = ObjectUtil.checkNotNull(headers, "headers"); + } + + @Override + public long id() { + return id; + } + + @Override + public Http3Headers headers() { + return headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultHttp3PushPromiseFrame that = (DefaultHttp3PushPromiseFrame) o; + return id == that.id && + Objects.equals(headers, that.headers); + } + + @Override + public int hashCode() { + return Objects.hash(id, headers); + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(id=" + id() + ", headers=" + headers() + ')'; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3SettingsFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3SettingsFrame.java new file mode 100644 index 0000000..654ff87 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3SettingsFrame.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.collection.LongObjectHashMap; +import io.netty.util.collection.LongObjectMap; +import io.netty.util.internal.StringUtil; + +import java.util.Iterator; +import java.util.Map; + +public final class DefaultHttp3SettingsFrame implements Http3SettingsFrame { + + private final LongObjectMap settings = new LongObjectHashMap<>(4); + + @Override + public Long get(long key) { + return settings.get(key); + } + + @Override + public Long put(long key, Long value) { + if (Http3CodecUtils.isReservedHttp2Setting(key)) { + throw new IllegalArgumentException("Setting is reserved for HTTP/2: " + key); + } + return settings.put(key, value); + } + + @Override + public Iterator> iterator() { + return settings.entrySet().iterator(); + } + + @Override + public int hashCode() { + return settings.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultHttp3SettingsFrame that = (DefaultHttp3SettingsFrame) o; + return that.settings.equals(settings); + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(settings=" + settings + ')'; + } + + /** + * Creates a new {@link DefaultHttp3SettingsFrame} which is a copy of the given settings. + * + * @param settingsFrame the frame to copy. + * @return the newly created copy. + */ + public static DefaultHttp3SettingsFrame copyOf(Http3SettingsFrame settingsFrame) { + DefaultHttp3SettingsFrame copy = new DefaultHttp3SettingsFrame(); + if (settingsFrame instanceof DefaultHttp3SettingsFrame) { + copy.settings.putAll(((DefaultHttp3SettingsFrame) settingsFrame).settings); + } else { + for (Map.Entry entry: settingsFrame) { + copy.put(entry.getKey(), entry.getValue()); + } + } + return copy; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3UnknownFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3UnknownFrame.java new file mode 100644 index 0000000..fb8aa79 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/DefaultHttp3UnknownFrame.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; +import io.netty.util.internal.StringUtil; + +import java.util.Objects; + +public final class DefaultHttp3UnknownFrame extends DefaultByteBufHolder implements Http3UnknownFrame { + private final long type; + + public DefaultHttp3UnknownFrame(long type, ByteBuf payload) { + super(payload); + this.type = Http3CodecUtils.checkIsReservedFrameType(type); + } + + @Override + public long type() { + return type; + } + + @Override + public Http3UnknownFrame copy() { + return new DefaultHttp3UnknownFrame(type, content().copy()); + } + + @Override + public Http3UnknownFrame duplicate() { + return new DefaultHttp3UnknownFrame(type, content().duplicate()); + } + + @Override + public Http3UnknownFrame retainedDuplicate() { + return new DefaultHttp3UnknownFrame(type, content().retainedDuplicate()); + } + + @Override + public Http3UnknownFrame replace(ByteBuf content) { + return new DefaultHttp3UnknownFrame(type, content); + } + + @Override + public Http3UnknownFrame retain() { + super.retain(); + return this; + } + + @Override + public Http3UnknownFrame retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public Http3UnknownFrame touch() { + super.touch(); + return this; + } + + @Override + public Http3UnknownFrame touch(Object hint) { + super.touch(hint); + return this; + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "(type=" + type() + ", content=" + content() + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultHttp3UnknownFrame that = (DefaultHttp3UnknownFrame) o; + if (type != that.type) { + return false; + } + return super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3.java new file mode 100644 index 0000000..4fed393 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3.java @@ -0,0 +1,176 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicClientCodecBuilder; +import io.netty.handler.codec.quic.QuicCodecBuilder; +import io.netty.handler.codec.quic.QuicServerCodecBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamChannelBootstrap; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; + +/** + * Contains utility methods that help to bootstrap server / clients with HTTP3 support. + */ +public final class Http3 { + + private Http3() { } + + private static final String[] H3_PROTOS = new String[] { + "h3-29", + "h3-30", + "h3-31", + "h3-32", + "h3" + }; + + private static final AttributeKey HTTP3_CONTROL_STREAM_KEY = + AttributeKey.valueOf(Http3.class, "HTTP3ControlStream"); + + private static final AttributeKey QPACK_ATTRIBUTES_KEY = + AttributeKey.valueOf(Http3.class, "QpackAttributes"); + + /** + * Returns the local initiated control stream for the HTTP/3 connection. + * @param channel the channel for the HTTP/3 connection. + * @return the control stream. + */ + public static QuicStreamChannel getLocalControlStream(Channel channel) { + return channel.attr(HTTP3_CONTROL_STREAM_KEY).get(); + } + + /** + * Returns the value of the max push ID received for + * this connection. + * + * @return Received max push ID for this + * connection. + */ + static long maxPushIdReceived(QuicChannel channel) { + final Http3ConnectionHandler connectionHandler = Http3CodecUtils.getConnectionHandlerOrClose(channel); + if (connectionHandler == null) { + throw new IllegalStateException("Connection handler not found."); + } + return connectionHandler.localControlStreamHandler.maxPushIdReceived(); + } + + static void setLocalControlStream(Channel channel, QuicStreamChannel controlStreamChannel) { + channel.attr(HTTP3_CONTROL_STREAM_KEY).set(controlStreamChannel); + } + + static QpackAttributes getQpackAttributes(Channel channel) { + return channel.attr(QPACK_ATTRIBUTES_KEY).get(); + } + + static void setQpackAttributes(Channel channel, QpackAttributes attributes) { + channel.attr(QPACK_ATTRIBUTES_KEY).set(attributes); + } + + /** + * Returns a new HTTP/3 request-stream that will use the given {@link ChannelHandler} + * to dispatch {@link Http3RequestStreamFrame}s too. The needed HTTP/3 codecs are automatically added to the + * pipeline as well. + * + * If you need more control you can also use the {@link Http3RequestStreamInitializer} directly. + * + * @param channel the {@link QuicChannel} for which we create the request-stream. + * @param handler the {@link ChannelHandler} to add. + * @return the {@link Future} that will be notified once the request-stream was opened. + */ + public static Future newRequestStream(QuicChannel channel, ChannelHandler handler) { + return channel.createStream(QuicStreamType.BIDIRECTIONAL, requestStreamInitializer(handler)); + } + + /** + * Returns a new HTTP/3 request-stream bootstrap that will use the given {@link ChannelHandler} + * to dispatch {@link Http3RequestStreamFrame}s too. The needed HTTP/3 codecs are automatically added to the + * pipeline as well. + * + * If you need more control you can also use the {@link Http3RequestStreamInitializer} directly. + * + * @param channel the {@link QuicChannel} for which we create the request-stream. + * @param handler the {@link ChannelHandler} to add. + * @return the {@link QuicStreamChannelBootstrap} that should be used. + */ + public static QuicStreamChannelBootstrap newRequestStreamBootstrap(QuicChannel channel, ChannelHandler handler) { + return channel.newStreamBootstrap().handler(requestStreamInitializer(handler)) + .type(QuicStreamType.BIDIRECTIONAL); + } + + /** + * Returns the supported protocols for H3. + * + * @return the supported protocols. + */ + public static String[] supportedApplicationProtocols() { + return H3_PROTOS.clone(); + } + + /** + * + * Minimum number max unidirectional streams. + */ + // control-stream, qpack decoder stream, qpack encoder stream + public static final int MIN_INITIAL_MAX_STREAMS_UNIDIRECTIONAL = 3; + + /** + * + * Minimum max data for unidirectional streams. + */ + public static final int MIN_INITIAL_MAX_STREAM_DATA_UNIDIRECTIONAL = 1024; + + /** + * Returns a new {@link QuicServerCodecBuilder} that has preconfigured for HTTP3. + * + * @return a pre-configured builder for HTTP3. + */ + public static QuicServerCodecBuilder newQuicServerCodecBuilder() { + return configure(new QuicServerCodecBuilder()); + } + + /** + * Returns a new {@link QuicClientCodecBuilder} that has preconfigured for HTTP3. + * + * @return a pre-configured builder for HTTP3. + */ + public static QuicClientCodecBuilder newQuicClientCodecBuilder() { + return configure(new QuicClientCodecBuilder()); + } + + private static > T configure(T builder) { + return builder.initialMaxStreamsUnidirectional(MIN_INITIAL_MAX_STREAMS_UNIDIRECTIONAL) + .initialMaxStreamDataUnidirectional(MIN_INITIAL_MAX_STREAM_DATA_UNIDIRECTIONAL); + } + + private static Http3RequestStreamInitializer requestStreamInitializer(ChannelHandler handler) { + if (handler instanceof Http3RequestStreamInitializer) { + return (Http3RequestStreamInitializer) handler; + } + return new Http3RequestStreamInitializer() { + @Override + protected void initRequestStream(QuicStreamChannel ch) { + ch.pipeline().addLast(handler); + } + }; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3CancelPushFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3CancelPushFrame.java new file mode 100644 index 0000000..caf3373 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3CancelPushFrame.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * See CANCEL_PUSH. + */ +public interface Http3CancelPushFrame extends Http3ControlStreamFrame { + + @Override + default long type() { + return Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE; + } + + /** + * Returns the push id that identifies the server push that is being cancelled. + * + * @return the id. + */ + long id(); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ClientConnectionHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ClientConnectionHandler.java new file mode 100644 index 0000000..1056fd0 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ClientConnectionHandler.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.quic.QuicStreamChannel; + +import java.util.function.LongFunction; + +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY; + +public final class Http3ClientConnectionHandler extends Http3ConnectionHandler { + + private final LongFunction pushStreamHandlerFactory; + + /** + * Create a new instance. + */ + public Http3ClientConnectionHandler() { + this(null, null, null, null, true); + } + + /** + * Create a new instance. + * + * @param inboundControlStreamHandler the {@link ChannelHandler} which will be notified about + * {@link Http3RequestStreamFrame}s or {@code null} if the user is not + * interested in these. + * @param pushStreamHandlerFactory the {@link LongFunction} that will provide a custom + * {@link ChannelHandler} for push streams {@code null} if no special + * handling should be done. When present, push ID will be passed as an + * argument to the {@link LongFunction}. + * @param unknownInboundStreamHandlerFactory the {@link LongFunction} that will provide a custom + * {@link ChannelHandler} for unknown inbound stream types or + * {@code null} if no special handling should be done. + * @param localSettings the local {@link Http3SettingsFrame} that should be sent to the + * remote peer or {@code null} if the default settings should be used. + * @param disableQpackDynamicTable If QPACK dynamic table should be disabled. + */ + public Http3ClientConnectionHandler(ChannelHandler inboundControlStreamHandler, + LongFunction pushStreamHandlerFactory, + LongFunction unknownInboundStreamHandlerFactory, + Http3SettingsFrame localSettings, boolean disableQpackDynamicTable) { + super(false, inboundControlStreamHandler, unknownInboundStreamHandlerFactory, localSettings, + disableQpackDynamicTable); + this.pushStreamHandlerFactory = pushStreamHandlerFactory; + } + + @Override + void initBidirectionalStream(ChannelHandlerContext ctx, QuicStreamChannel channel) { + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.1 + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_STREAM_CREATION_ERROR, + "Server initiated bidirectional streams are not allowed", true); + } + + @Override + void initUnidirectionalStream(ChannelHandlerContext ctx, QuicStreamChannel streamChannel) { + final Long maxTableCapacity = remoteControlStreamHandler.localSettings() + .get(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY); + streamChannel.pipeline().addLast( + new Http3UnidirectionalStreamInboundClientHandler(codecFactory, + localControlStreamHandler, remoteControlStreamHandler, + unknownInboundStreamHandlerFactory, pushStreamHandlerFactory, + () -> new QpackEncoderHandler(maxTableCapacity, qpackDecoder), + () -> new QpackDecoderHandler(qpackEncoder))); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3CodecUtils.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3CodecUtils.java new file mode 100644 index 0000000..bdb9c60 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3CodecUtils.java @@ -0,0 +1,331 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + +import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_INTERNAL_ERROR; +import static io.netty.handler.codec.quic.QuicStreamType.UNIDIRECTIONAL; + +final class Http3CodecUtils { + + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8 + static final long MIN_RESERVED_FRAME_TYPE = 0x1f * 1 + 0x21; + static final long MAX_RESERVED_FRAME_TYPE = 0x1f * (long) Integer.MAX_VALUE + 0x21; + + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2 + static final int HTTP3_DATA_FRAME_TYPE = 0x0; + static final int HTTP3_HEADERS_FRAME_TYPE = 0x1; + static final int HTTP3_CANCEL_PUSH_FRAME_TYPE = 0x3; + static final int HTTP3_SETTINGS_FRAME_TYPE = 0x4; + static final int HTTP3_PUSH_PROMISE_FRAME_TYPE = 0x5; + static final int HTTP3_GO_AWAY_FRAME_TYPE = 0x7; + static final int HTTP3_MAX_PUSH_ID_FRAME_TYPE = 0xd; + + static final int HTTP3_CANCEL_PUSH_FRAME_MAX_LEN = 8; + static final int HTTP3_SETTINGS_FRAME_MAX_LEN = 256; + static final int HTTP3_GO_AWAY_FRAME_MAX_LEN = 8; + static final int HTTP3_MAX_PUSH_ID_FRAME_MAX_LEN = 8; + + static final int HTTP3_CONTROL_STREAM_TYPE = 0x00; + static final int HTTP3_PUSH_STREAM_TYPE = 0x01; + static final int HTTP3_QPACK_ENCODER_STREAM_TYPE = 0x02; + static final int HTTP3_QPACK_DECODER_STREAM_TYPE = 0x03; + + private Http3CodecUtils() { } + + static long checkIsReservedFrameType(long type) { + return ObjectUtil.checkInRange(type, MIN_RESERVED_FRAME_TYPE, MAX_RESERVED_FRAME_TYPE, "type"); + } + + static boolean isReservedFrameType(long type) { + return type >= MIN_RESERVED_FRAME_TYPE && type <= MAX_RESERVED_FRAME_TYPE; + } + + /** + * Checks if the passed {@link QuicStreamChannel} is a server initiated stream. + * + * @param channel to check. + * @return {@code true} if the passed {@link QuicStreamChannel} is a server initiated stream. + */ + static boolean isServerInitiatedQuicStream(QuicStreamChannel channel) { + // Server streams have odd stream id + // https://www.rfc-editor.org/rfc/rfc9000.html#name-stream-types-and-identifier + return channel.streamId() % 2 != 0; + } + + static boolean isReservedHttp2FrameType(long type) { + switch ((int) type) { + // Reserved types that were used in HTTP/2 + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-11.2.1 + case 0x2: + case 0x6: + case 0x8: + case 0x9: + return true; + default: + return false; + } + } + + static boolean isReservedHttp2Setting(long key) { + switch ((int) key) { + // Reserved types that were used in HTTP/2 + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-11.2.2 + case 0x2: + case 0x3: + case 0x4: + case 0x5: + return true; + default: + return false; + } + } + + /** + * Returns the number of bytes needed to encode the variable length integer. + * + * See + * Variable-Length Integer Encoding. + */ + static int numBytesForVariableLengthInteger(long value) { + if (value <= 63) { + return 1; + } + if (value <= 16383) { + return 2; + } + if (value <= 1073741823) { + return 4; + } + if (value <= 4611686018427387903L) { + return 8; + } + throw new IllegalArgumentException(); + } + + /** + * Write the variable length integer into the {@link ByteBuf}. + * + * See + * Variable-Length Integer Encoding. + */ + static void writeVariableLengthInteger(ByteBuf out, long value) { + int numBytes = numBytesForVariableLengthInteger(value); + writeVariableLengthInteger(out, value, numBytes); + } + + /** + * Write the variable length integer into the {@link ByteBuf}. + * + * See + * Variable-Length Integer Encoding. + */ + static void writeVariableLengthInteger(ByteBuf out, long value, int numBytes) { + int writerIndex = out.writerIndex(); + switch (numBytes) { + case 1: + out.writeByte((byte) value); + break; + case 2: + out.writeShort((short) value); + encodeLengthIntoBuffer(out, writerIndex, (byte) 0x40); + break; + case 4: + out.writeInt((int) value); + encodeLengthIntoBuffer(out, writerIndex, (byte) 0x80); + break; + case 8: + out.writeLong(value); + encodeLengthIntoBuffer(out, writerIndex, (byte) 0xc0); + break; + default: + throw new IllegalArgumentException(); + } + } + + private static void encodeLengthIntoBuffer(ByteBuf out, int index, byte b) { + out.setByte(index, out.getByte(index) | b); + } + + /** + * Read the variable length integer from the {@link ByteBuf}. + * + * See + * Variable-Length Integer Encoding + */ + static long readVariableLengthInteger(ByteBuf in, int len) { + switch (len) { + case 1: + return in.readUnsignedByte(); + case 2: + return in.readUnsignedShort() & 0x3fff; + case 4: + return in.readUnsignedInt() & 0x3fffffff; + case 8: + return in.readLong() & 0x3fffffffffffffffL; + default: + throw new IllegalArgumentException(); + } + } + + /** + * Returns the number of bytes that were encoded into the byte for a variable length integer to read. + * + * See + * Variable-Length Integer Encoding + */ + static int numBytesForVariableLengthInteger(byte b) { + byte val = (byte) (b >> 6); + if ((val & 1) != 0) { + if ((val & 2) != 0) { + return 8; + } + return 2; + } + if ((val & 2) != 0) { + return 4; + } + return 1; + } + + static void criticalStreamClosed(ChannelHandlerContext ctx) { + if (ctx.channel().parent().isActive()) { + // Stream was closed while the parent channel is still active + Http3CodecUtils.connectionError( + ctx, Http3ErrorCode.H3_CLOSED_CRITICAL_STREAM, "Critical stream closed.", false); + } + } + + /** + * A connection-error should be handled as defined in the HTTP3 spec. + * @param ctx the {@link ChannelHandlerContext} of the handle that handles it. + * @param exception the {@link Http3Exception} that caused the error. + * @param fireException {@code true} if we should also fire the {@link Http3Exception} through the pipeline. + */ + static void connectionError(ChannelHandlerContext ctx, Http3Exception exception, boolean fireException) { + if (fireException) { + ctx.fireExceptionCaught(exception); + } + connectionError(ctx.channel(), exception.errorCode(), exception.getMessage()); + } + + /** + * A connection-error should be handled as defined in the HTTP3 spec. + * + * @param ctx the {@link ChannelHandlerContext} of the handle that handles it. + * @param errorCode the {@link Http3ErrorCode} that caused the error. + * @param msg the message that should be used as reason for the error, may be {@code null}. + * @param fireException {@code true} if we should also fire the {@link Http3Exception} through the pipeline. + */ + static void connectionError(ChannelHandlerContext ctx, Http3ErrorCode errorCode, + String msg, boolean fireException) { + if (fireException) { + ctx.fireExceptionCaught(new Http3Exception(errorCode, msg)); + } + connectionError(ctx.channel(), errorCode, msg); + } + + /** + * Closes the channel if the passed {@link ChannelFuture} fails or has already failed. + * + * @param future {@link ChannelFuture} which if fails will close the channel. + */ + static void closeOnFailure(ChannelFuture future) { + if (future.isDone() && !future.isSuccess()) { + future.channel().close(); + return; + } + future.addListener(CLOSE_ON_FAILURE); + } + + /** + * A connection-error should be handled as defined in the HTTP3 spec. + * + * @param channel the {@link Channel} on which error has occured. + * @param errorCode the {@link Http3ErrorCode} that caused the error. + * @param msg the message that should be used as reason for the error, may be {@code null}. + */ + static void connectionError(Channel channel, Http3ErrorCode errorCode, String msg) { + final QuicChannel quicChannel; + + if (channel instanceof QuicChannel) { + quicChannel = (QuicChannel) channel; + } else { + quicChannel = (QuicChannel) channel.parent(); + } + final ByteBuf buffer; + if (msg != null) { + // As we call an operation on the parent we should also use the parents allocator to allocate the buffer. + buffer = quicChannel.alloc().buffer(); + buffer.writeCharSequence(msg, CharsetUtil.US_ASCII); + } else { + buffer = Unpooled.EMPTY_BUFFER; + } + quicChannel.close(true, errorCode.code, buffer); + } + + static void streamError(ChannelHandlerContext ctx, Http3ErrorCode errorCode) { + ((QuicStreamChannel) ctx.channel()).shutdownOutput(errorCode.code); + } + + static void readIfNoAutoRead(ChannelHandlerContext ctx) { + if (!ctx.channel().config().isAutoRead()) { + ctx.read(); + } + } + + /** + * Retrieves {@link Http3ConnectionHandler} from the passed {@link QuicChannel} pipeline or closes the connection if + * none available. + * + * @param ch for which the {@link Http3ConnectionHandler} is to be retrieved. + * @return {@link Http3ConnectionHandler} if available, else close the connection and return {@code null}. + */ + static Http3ConnectionHandler getConnectionHandlerOrClose(QuicChannel ch) { + Http3ConnectionHandler connectionHandler = ch.pipeline().get(Http3ConnectionHandler.class); + if (connectionHandler == null) { + connectionError(ch, H3_INTERNAL_ERROR, "Couldn't obtain the " + + StringUtil.simpleClassName(Http3ConnectionHandler.class) + " of the parent Channel"); + return null; + } + return connectionHandler; + } + + /** + * Verify if the passed {@link QuicStreamChannel} is a {@link QuicStreamType#UNIDIRECTIONAL} QUIC stream. + * + * @param ch to verify + * @throws IllegalArgumentException if the passed {@link QuicStreamChannel} is not a + * {@link QuicStreamType#UNIDIRECTIONAL} QUIC stream. + */ + static void verifyIsUnidirectional(QuicStreamChannel ch) { + if (ch.type() != UNIDIRECTIONAL) { + throw new IllegalArgumentException("Invalid stream type: " + ch.type() + " for stream: " + ch.streamId()); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ConnectionHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ConnectionHandler.java new file mode 100644 index 0000000..ff73cc9 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ConnectionHandler.java @@ -0,0 +1,203 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http3.Http3FrameCodec.Http3FrameCodecFactory; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; + +import java.util.function.LongFunction; + +import static io.netty.handler.codec.http3.Http3RequestStreamCodecState.NO_STATE; +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS; +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY; +import static java.lang.Math.toIntExact; + +/** + * Handler that handles HTTP3 connections. + */ +public abstract class Http3ConnectionHandler extends ChannelInboundHandlerAdapter { + final Http3FrameCodecFactory codecFactory; + final LongFunction unknownInboundStreamHandlerFactory; + final boolean disableQpackDynamicTable; + final Http3ControlStreamInboundHandler localControlStreamHandler; + final Http3ControlStreamOutboundHandler remoteControlStreamHandler; + final QpackDecoder qpackDecoder; + final QpackEncoder qpackEncoder; + private boolean controlStreamCreationInProgress; + + /** + * Create a new instance. + * @param server {@code true} if server-side, {@code false} otherwise. + * @param inboundControlStreamHandler the {@link ChannelHandler} which will be notified about + * {@link Http3RequestStreamFrame}s or {@code null} if the user is not + * interested in these. + * @param unknownInboundStreamHandlerFactory the {@link LongFunction} that will provide a custom + * {@link ChannelHandler} for unknown inbound stream types or + * {@code null} if no special handling should be done. + * @param localSettings the local {@link Http3SettingsFrame} that should be sent to the + * remote peer or {@code null} if the default settings should be used. + * @param disableQpackDynamicTable If QPACK dynamic table should be disabled. + */ + Http3ConnectionHandler(boolean server, ChannelHandler inboundControlStreamHandler, + LongFunction unknownInboundStreamHandlerFactory, + Http3SettingsFrame localSettings, boolean disableQpackDynamicTable) { + this.unknownInboundStreamHandlerFactory = unknownInboundStreamHandlerFactory; + this.disableQpackDynamicTable = disableQpackDynamicTable; + if (localSettings == null) { + localSettings = new DefaultHttp3SettingsFrame(); + } else { + localSettings = DefaultHttp3SettingsFrame.copyOf(localSettings); + } + Long maxFieldSectionSize = localSettings.get(Http3SettingsFrame.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE); + if (maxFieldSectionSize == null) { + // Just use the maximum value we can represent via a Long. + maxFieldSectionSize = Long.MAX_VALUE; + } + long maxTableCapacity = localSettings.getOrDefault(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, 0); + int maxBlockedStreams = toIntExact(localSettings.getOrDefault(HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS, 0)); + qpackDecoder = new QpackDecoder(maxTableCapacity, maxBlockedStreams); + qpackEncoder = new QpackEncoder(); + codecFactory = Http3FrameCodec.newFactory(qpackDecoder, maxFieldSectionSize, qpackEncoder); + remoteControlStreamHandler = new Http3ControlStreamOutboundHandler(server, localSettings, + codecFactory.newCodec(Http3FrameTypeValidator.NO_VALIDATION, NO_STATE, NO_STATE)); + localControlStreamHandler = new Http3ControlStreamInboundHandler(server, inboundControlStreamHandler, + qpackEncoder, remoteControlStreamHandler); + } + + private void createControlStreamIfNeeded(ChannelHandlerContext ctx) { + if (!controlStreamCreationInProgress && Http3.getLocalControlStream(ctx.channel()) == null) { + controlStreamCreationInProgress = true; + QuicChannel channel = (QuicChannel) ctx.channel(); + // Once the channel became active we need to create an unidirectional stream and write the + // Http3SettingsFrame to it. This needs to be the first frame on this stream. + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1. + channel.createStream(QuicStreamType.UNIDIRECTIONAL, remoteControlStreamHandler) + .addListener(f -> { + if (!f.isSuccess()) { + ctx.fireExceptionCaught(new Http3Exception(Http3ErrorCode.H3_STREAM_CREATION_ERROR, + "Unable to open control stream", f.cause())); + ctx.close(); + } else { + Http3.setLocalControlStream(channel, (QuicStreamChannel) f.getNow()); + } + }); + } + } + + /** + * Returns {@code true} if we received a GOAWAY frame from the remote peer. + * @return {@code true} if we received the frame, {@code false} otherwise. + */ + public final boolean isGoAwayReceived() { + return localControlStreamHandler.isGoAwayReceived(); + } + + /** + * Returns a new codec that will encode and decode {@link Http3Frame}s for this HTTP/3 connection. + * + * @return a new codec. + */ + public final ChannelHandler newCodec(Http3RequestStreamCodecState encodeState, + Http3RequestStreamCodecState decodeState) { + return codecFactory.newCodec(Http3RequestStreamFrameTypeValidator.INSTANCE, encodeState, decodeState); + } + + final ChannelHandler newRequestStreamValidationHandler( + QuicStreamChannel forStream, Http3RequestStreamCodecState encodeState, + Http3RequestStreamCodecState decodeState) { + final QpackAttributes qpackAttributes = Http3.getQpackAttributes(forStream.parent()); + assert qpackAttributes != null; + if (localControlStreamHandler.isServer()) { + return Http3RequestStreamValidationHandler.newServerValidator(qpackAttributes, qpackDecoder, + encodeState, decodeState); + } + return Http3RequestStreamValidationHandler.newClientValidator(localControlStreamHandler::isGoAwayReceived, + qpackAttributes, qpackDecoder, encodeState, decodeState); + } + + final ChannelHandler newPushStreamValidationHandler(QuicStreamChannel forStream, + Http3RequestStreamCodecState decodeState) { + if (localControlStreamHandler.isServer()) { + return Http3PushStreamServerValidationHandler.INSTANCE; + } + final QpackAttributes qpackAttributes = Http3.getQpackAttributes(forStream.parent()); + assert qpackAttributes != null; + return new Http3PushStreamClientValidationHandler(qpackAttributes, qpackDecoder, decodeState); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + Http3.setQpackAttributes(channel, new QpackAttributes(channel, disableQpackDynamicTable)); + if (ctx.channel().isActive()) { + createControlStreamIfNeeded(ctx); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + createControlStreamIfNeeded(ctx); + + ctx.fireChannelActive(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof QuicStreamChannel) { + QuicStreamChannel channel = (QuicStreamChannel) msg; + switch (channel.type()) { + case BIDIRECTIONAL: + initBidirectionalStream(ctx, channel); + break; + case UNIDIRECTIONAL: + initUnidirectionalStream(ctx, channel); + break; + default: + throw new Error(); + } + } + ctx.fireChannelRead(msg); + } + + /** + * Called when an bidirectional stream is opened from the remote-peer. + * + * @param ctx the {@link ChannelHandlerContext} of the parent {@link QuicChannel}. + * @param streamChannel the {@link QuicStreamChannel}. + */ + abstract void initBidirectionalStream(ChannelHandlerContext ctx, QuicStreamChannel streamChannel); + + /** + * Called when an unidirectional stream is opened from the remote-peer. + * + * @param ctx the {@link ChannelHandlerContext} of the parent {@link QuicChannel}. + * @param streamChannel the {@link QuicStreamChannel}. + */ + abstract void initUnidirectionalStream(ChannelHandlerContext ctx, QuicStreamChannel streamChannel); + + /** + * Always returns {@code false} as it keeps state. + */ + @Override + public boolean isSharable() { + return false; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamFrame.java new file mode 100644 index 0000000..7e55bf1 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamFrame.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * Marker interface for frames that can be sent and received on a + * HTTP3 control stream. + */ +public interface Http3ControlStreamFrame extends Http3Frame { +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamFrameTypeValidator.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamFrameTypeValidator.java new file mode 100644 index 0000000..0f44dc5 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamFrameTypeValidator.java @@ -0,0 +1,43 @@ +/* + * 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.handler.codec.http3; + +/** + * Validate that the frame type is valid for a control stream. + */ +final class Http3ControlStreamFrameTypeValidator implements Http3FrameTypeValidator { + + static final Http3ControlStreamFrameTypeValidator INSTANCE = new Http3ControlStreamFrameTypeValidator(); + + private Http3ControlStreamFrameTypeValidator() { } + + @Override + public void validate(long type, boolean first) throws Http3Exception { + switch ((int) type) { + case Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE: + case Http3CodecUtils.HTTP3_HEADERS_FRAME_TYPE: + case Http3CodecUtils.HTTP3_DATA_FRAME_TYPE: + if (first) { + throw new Http3Exception(Http3ErrorCode.H3_MISSING_SETTINGS, + "Missing settings frame."); + } + throw new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Unexpected frame type '" + type + "' received"); + default: + break; + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamInboundHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamInboundHandler.java new file mode 100644 index 0000000..97bddfa --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamInboundHandler.java @@ -0,0 +1,319 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; + +import java.nio.channels.ClosedChannelException; + +import static io.netty.handler.codec.http3.Http3CodecUtils.closeOnFailure; +import static io.netty.handler.codec.http3.Http3CodecUtils.connectionError; +import static io.netty.handler.codec.http3.Http3CodecUtils.criticalStreamClosed; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_FRAME_UNEXPECTED; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_ID_ERROR; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_MISSING_SETTINGS; +import static io.netty.handler.codec.http3.Http3ErrorCode.QPACK_ENCODER_STREAM_ERROR; +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS; +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY; +import static io.netty.handler.codec.http3.QpackUtil.toIntOrThrow; +import static io.netty.util.internal.ThrowableUtil.unknownStackTrace; + +final class Http3ControlStreamInboundHandler extends Http3FrameTypeInboundValidationHandler { + final boolean server; + private final ChannelHandler controlFrameHandler; + private final QpackEncoder qpackEncoder; + private final Http3ControlStreamOutboundHandler remoteControlStreamHandler; + private boolean firstFrameRead; + private Long receivedGoawayId; + private Long receivedMaxPushId; + + Http3ControlStreamInboundHandler(boolean server, ChannelHandler controlFrameHandler, QpackEncoder qpackEncoder, + Http3ControlStreamOutboundHandler remoteControlStreamHandler) { + super(Http3ControlStreamFrame.class); + this.server = server; + this.controlFrameHandler = controlFrameHandler; + this.qpackEncoder = qpackEncoder; + this.remoteControlStreamHandler = remoteControlStreamHandler; + } + + boolean isServer() { + return server; + } + + boolean isGoAwayReceived() { + return receivedGoawayId != null; + } + + long maxPushIdReceived() { + return receivedMaxPushId == null ? -1 : receivedMaxPushId; + } + + private boolean forwardControlFrames() { + return controlFrameHandler != null; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + super.handlerAdded(ctx); + // The user want's to be notified about control frames, add the handler to the pipeline. + if (controlFrameHandler != null) { + ctx.pipeline().addLast(controlFrameHandler); + } + } + + @Override + void readFrameDiscarded(ChannelHandlerContext ctx, Object discardedFrame) { + if (!firstFrameRead && !(discardedFrame instanceof Http3SettingsFrame)) { + connectionError(ctx, Http3ErrorCode.H3_MISSING_SETTINGS, "Missing settings frame.", forwardControlFrames()); + } + } + + @Override + void channelRead(ChannelHandlerContext ctx, Http3ControlStreamFrame frame) throws QpackException { + boolean isSettingsFrame = frame instanceof Http3SettingsFrame; + if (!firstFrameRead && !isSettingsFrame) { + connectionError(ctx, H3_MISSING_SETTINGS, "Missing settings frame.", forwardControlFrames()); + ReferenceCountUtil.release(frame); + return; + } + if (firstFrameRead && isSettingsFrame) { + connectionError(ctx, H3_FRAME_UNEXPECTED, "Second settings frame received.", forwardControlFrames()); + ReferenceCountUtil.release(frame); + return; + } + firstFrameRead = true; + + final boolean valid; + if (isSettingsFrame) { + valid = handleHttp3SettingsFrame(ctx, (Http3SettingsFrame) frame); + } else if (frame instanceof Http3GoAwayFrame) { + valid = handleHttp3GoAwayFrame(ctx, (Http3GoAwayFrame) frame); + } else if (frame instanceof Http3MaxPushIdFrame) { + valid = handleHttp3MaxPushIdFrame(ctx, (Http3MaxPushIdFrame) frame); + } else if (frame instanceof Http3CancelPushFrame) { + valid = handleHttp3CancelPushFrame(ctx, (Http3CancelPushFrame) frame); + } else { + // We don't need to do any special handling for Http3UnknownFrames as we either pass these to the next# + // handler or release these directly. + assert frame instanceof Http3UnknownFrame; + valid = true; + } + + if (!valid || controlFrameHandler == null) { + ReferenceCountUtil.release(frame); + return; + } + + // The user did specify ChannelHandler that should be notified about control stream frames. + // Let's forward the frame so the user can do something with it. + ctx.fireChannelRead(frame); + } + + private boolean handleHttp3SettingsFrame(ChannelHandlerContext ctx, Http3SettingsFrame settingsFrame) + throws QpackException { + final QuicChannel quicChannel = (QuicChannel) ctx.channel().parent(); + final QpackAttributes qpackAttributes = Http3.getQpackAttributes(quicChannel); + assert qpackAttributes != null; + final GenericFutureListener> closeOnFailure = future -> { + if (!future.isSuccess()) { + criticalStreamClosed(ctx); + } + }; + if (qpackAttributes.dynamicTableDisabled()) { + qpackEncoder.configureDynamicTable(qpackAttributes, 0, 0); + return true; + } + quicChannel.createStream(QuicStreamType.UNIDIRECTIONAL, + new QPackEncoderStreamInitializer(qpackEncoder, qpackAttributes, + settingsFrame.getOrDefault(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, 0), + settingsFrame.getOrDefault(HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS, 0))) + .addListener(closeOnFailure); + quicChannel.createStream(QuicStreamType.UNIDIRECTIONAL, new QPackDecoderStreamInitializer(qpackAttributes)) + .addListener(closeOnFailure); + return true; + } + + private boolean handleHttp3GoAwayFrame(ChannelHandlerContext ctx, Http3GoAwayFrame goAwayFrame) { + long id = goAwayFrame.id(); + if (!server && id % 4 != 0) { + connectionError(ctx, H3_FRAME_UNEXPECTED, "GOAWAY received with ID of non-request stream.", + forwardControlFrames()); + return false; + } + if (receivedGoawayId != null && id > receivedGoawayId) { + connectionError(ctx, H3_ID_ERROR, + "GOAWAY received with ID larger than previously received.", forwardControlFrames()); + return false; + } + receivedGoawayId = id; + return true; + } + + private boolean handleHttp3MaxPushIdFrame(ChannelHandlerContext ctx, Http3MaxPushIdFrame frame) { + long id = frame.id(); + if (!server) { + connectionError(ctx, H3_FRAME_UNEXPECTED, "MAX_PUSH_ID received by client.", + forwardControlFrames()); + return false; + } + if (receivedMaxPushId != null && id < receivedMaxPushId) { + connectionError(ctx, H3_ID_ERROR, "MAX_PUSH_ID reduced limit.", forwardControlFrames()); + return false; + } + receivedMaxPushId = id; + return true; + } + + private boolean handleHttp3CancelPushFrame(ChannelHandlerContext ctx, Http3CancelPushFrame cancelPushFrame) { + final Long maxPushId = server ? receivedMaxPushId : remoteControlStreamHandler.sentMaxPushId(); + if (maxPushId == null || maxPushId < cancelPushFrame.id()) { + connectionError(ctx, H3_ID_ERROR, "CANCEL_PUSH received with an ID greater than MAX_PUSH_ID.", + forwardControlFrames()); + return false; + } + return true; + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.fireChannelReadComplete(); + + // control streams should always be processed, no matter what the user is doing in terms of + // configuration and AUTO_READ. + Http3CodecUtils.readIfNoAutoRead(ctx); + } + + @Override + public boolean isSharable() { + // Not sharable as it keeps state. + return false; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof ChannelInputShutdownEvent) { + // See https://www.ietf.org/archive/id/draft-ietf-quic-qpack-19.html#section-4.2 + criticalStreamClosed(ctx); + } + ctx.fireUserEventTriggered(evt); + } + + private abstract static class AbstractQPackStreamInitializer extends ChannelInboundHandlerAdapter { + private final int streamType; + protected final QpackAttributes attributes; + + AbstractQPackStreamInitializer(int streamType, QpackAttributes attributes) { + this.streamType = streamType; + this.attributes = attributes; + } + + @Override + public final void channelActive(ChannelHandlerContext ctx) { + // We need to write the streamType into the stream before doing anything else. + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1 + // Just allocate 8 bytes which would be the max needed. + ByteBuf buffer = ctx.alloc().buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, streamType); + closeOnFailure(ctx.writeAndFlush(buffer)); + streamAvailable(ctx); + ctx.fireChannelActive(); + } + + @Override + public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + streamClosed(ctx); + if (evt instanceof ChannelInputShutdownEvent) { + // See https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 + criticalStreamClosed(ctx); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + streamClosed(ctx); + // See https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 + criticalStreamClosed(ctx); + ctx.fireChannelInactive(); + } + + protected abstract void streamAvailable(ChannelHandlerContext ctx); + + protected abstract void streamClosed(ChannelHandlerContext ctx); + } + + private static final class QPackEncoderStreamInitializer extends AbstractQPackStreamInitializer { + private static final ClosedChannelException ENCODER_STREAM_INACTIVE = + unknownStackTrace(new ClosedChannelException(), ClosedChannelException.class, "streamClosed()"); + private final QpackEncoder encoder; + private final long maxTableCapacity; + private final long blockedStreams; + + QPackEncoderStreamInitializer(QpackEncoder encoder, QpackAttributes attributes, long maxTableCapacity, + long blockedStreams) { + super(Http3CodecUtils.HTTP3_QPACK_ENCODER_STREAM_TYPE, attributes); + this.encoder = encoder; + this.maxTableCapacity = maxTableCapacity; + this.blockedStreams = blockedStreams; + } + + @Override + protected void streamAvailable(ChannelHandlerContext ctx) { + final QuicStreamChannel stream = (QuicStreamChannel) ctx.channel(); + attributes.encoderStream(stream); + + try { + encoder.configureDynamicTable(attributes, maxTableCapacity, toIntOrThrow(blockedStreams)); + } catch (QpackException e) { + connectionError(ctx, new Http3Exception(QPACK_ENCODER_STREAM_ERROR, + "Dynamic table configuration failed.", e), true); + } + } + + @Override + protected void streamClosed(ChannelHandlerContext ctx) { + attributes.encoderStreamInactive(ENCODER_STREAM_INACTIVE); + } + } + + private static final class QPackDecoderStreamInitializer extends AbstractQPackStreamInitializer { + private static final ClosedChannelException DECODER_STREAM_INACTIVE = + unknownStackTrace(new ClosedChannelException(), ClosedChannelException.class, "streamClosed()"); + private QPackDecoderStreamInitializer(QpackAttributes attributes) { + super(Http3CodecUtils.HTTP3_QPACK_DECODER_STREAM_TYPE, attributes); + } + + @Override + protected void streamAvailable(ChannelHandlerContext ctx) { + attributes.decoderStream((QuicStreamChannel) ctx.channel()); + } + + @Override + protected void streamClosed(ChannelHandlerContext ctx) { + attributes.decoderStreamInactive(DECODER_STREAM_INACTIVE); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamOutboundHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamOutboundHandler.java new file mode 100644 index 0000000..a2b39f0 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ControlStreamOutboundHandler.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.ObjectUtil; + +import static io.netty.handler.codec.http3.Http3CodecUtils.closeOnFailure; + +final class Http3ControlStreamOutboundHandler + extends Http3FrameTypeDuplexValidationHandler { + private final boolean server; + private final Http3SettingsFrame localSettings; + private final ChannelHandler codec; + private Long sentMaxPushId; + private Long sendGoAwayId; + + Http3ControlStreamOutboundHandler(boolean server, Http3SettingsFrame localSettings, ChannelHandler codec) { + super(Http3ControlStreamFrame.class); + this.server = server; + this.localSettings = ObjectUtil.checkNotNull(localSettings, "localSettings"); + this.codec = ObjectUtil.checkNotNull(codec, "codec"); + } + + /** + * Returns the local settings that were sent on the control stream. + * + * @return the local {@link Http3SettingsFrame}. + */ + Http3SettingsFrame localSettings() { + return localSettings; + } + + /** + * Returns the last id that was sent in a MAX_PUSH_ID frame or {@code null} if none was sent yet. + * + * @return the id. + */ + Long sentMaxPushId() { + return sentMaxPushId; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + // We need to write 0x00 into the stream before doing anything else. + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1 + // Just allocate 8 bytes which would be the max needed. + ByteBuf buffer = ctx.alloc().buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, Http3CodecUtils.HTTP3_CONTROL_STREAM_TYPE); + ctx.write(buffer); + // Add the encoder and decoder in the pipeline so we can handle Http3Frames. This needs to happen after + // we did write the type via a ByteBuf. + ctx.pipeline().addFirst(codec); + // If writing of the local settings fails let's just teardown the connection. + closeOnFailure(ctx.writeAndFlush(DefaultHttp3SettingsFrame.copyOf(localSettings))); + + ctx.fireChannelActive(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof ChannelInputShutdownEvent) { + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1 + Http3CodecUtils.criticalStreamClosed(ctx); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1 + Http3CodecUtils.criticalStreamClosed(ctx); + ctx.fireChannelInactive(); + } + + @Override + void write(ChannelHandlerContext ctx, Http3ControlStreamFrame msg, ChannelPromise promise) { + if (msg instanceof Http3MaxPushIdFrame && !handleHttp3MaxPushIdFrame(promise, (Http3MaxPushIdFrame) msg)) { + ReferenceCountUtil.release(msg); + return; + } else if (msg instanceof Http3GoAwayFrame && !handleHttp3GoAwayFrame(promise, (Http3GoAwayFrame) msg)) { + ReferenceCountUtil.release(msg); + return; + } + + ctx.write(msg, promise); + } + + private boolean handleHttp3MaxPushIdFrame(ChannelPromise promise, Http3MaxPushIdFrame maxPushIdFrame) { + long id = maxPushIdFrame.id(); + + // See https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-32#section-7.2.7 + if (sentMaxPushId != null && id < sentMaxPushId) { + promise.setFailure(new Http3Exception(Http3ErrorCode.H3_ID_ERROR, "MAX_PUSH_ID reduced limit.")); + return false; + } + + sentMaxPushId = maxPushIdFrame.id(); + return true; + } + + private boolean handleHttp3GoAwayFrame(ChannelPromise promise, Http3GoAwayFrame goAwayFrame) { + long id = goAwayFrame.id(); + + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-5.2 + if (server && id % 4 != 0) { + promise.setFailure(new Http3Exception(Http3ErrorCode.H3_ID_ERROR, + "GOAWAY id not valid : " + id)); + return false; + } + + if (sendGoAwayId != null && id > sendGoAwayId) { + promise.setFailure(new Http3Exception(Http3ErrorCode.H3_ID_ERROR, + "GOAWAY id is bigger then the last sent: " + id + " > " + sendGoAwayId)); + return false; + } + + sendGoAwayId = id; + return true; + } + + @Override + public boolean isSharable() { + // This handle keeps state so we cant reuse it. + return false; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3DataFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3DataFrame.java new file mode 100644 index 0000000..e303701 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3DataFrame.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; + +/** + * See DATA. + */ +public interface Http3DataFrame extends ByteBufHolder, Http3RequestStreamFrame, Http3PushStreamFrame { + + @Override + default long type() { + return Http3CodecUtils.HTTP3_DATA_FRAME_TYPE; + } + + @Override + Http3DataFrame copy(); + + @Override + Http3DataFrame duplicate(); + + @Override + Http3DataFrame retainedDuplicate(); + + @Override + Http3DataFrame replace(ByteBuf content); + + @Override + Http3DataFrame retain(); + + @Override + Http3DataFrame retain(int increment); + + @Override + Http3DataFrame touch(); + + @Override + Http3DataFrame touch(Object hint); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ErrorCode.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ErrorCode.java new file mode 100644 index 0000000..c079ad2 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ErrorCode.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * Different HTTP3 error codes. + */ +public enum Http3ErrorCode { + + /** + * No error. This is used when the connection or stream needs to be closed, but there is no error to signal. + */ + H3_NO_ERROR(0x100), + + /** + * Peer violated protocol requirements in a way that does not match a more specific error code, + * or endpoint declines to use the more specific error code. + */ + H3_GENERAL_PROTOCOL_ERROR(0x101), + + /** + * An internal error has occurred in the HTTP stack. + */ + H3_INTERNAL_ERROR(0x102), + + /** + * The endpoint detected that its peer created a stream that it will not accept. + */ + H3_STREAM_CREATION_ERROR(0x103), + + /** + * A stream required by the HTTP/3 connection was closed or reset. + */ + H3_CLOSED_CRITICAL_STREAM(0x104), + + /** + * A frame was received that was not permitted in the current state or on the current stream. + */ + H3_FRAME_UNEXPECTED(0x105), + + /** + * A frame that fails to satisfy layout requirements or with an invalid size was received. + */ + H3_FRAME_ERROR(0x106), + + /** + * The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load. + */ + H3_EXCESSIVE_LOAD(0x107), + + /** + * A Stream ID or Push ID was used incorrectly, such as exceeding a limit, reducing a limit, or being reused. + */ + H3_ID_ERROR(0x108), + + /** + * An endpoint detected an error in the payload of a SETTINGS frame. + */ + H3_SETTINGS_ERROR(0x109), + + /** + * No SETTINGS frame was received at the beginning of the control stream. + */ + H3_MISSING_SETTINGS(0x10a), + + /** + * A server rejected a request without performing any application processing. + */ + H3_REQUEST_REJECTED(0x10b), + + /** + * The request or its response (including pushed response) is cancelled. + */ + H3_REQUEST_CANCELLED(0x10c), + + /** + * The client's stream terminated without containing a fully-formed request. + */ + H3_REQUEST_INCOMPLETE(0x10d), + + /** + * An HTTP message was malformed and cannot be processed. + */ + H3_MESSAGE_ERROR(0x10e), + + /** + * The TCP connection established in response to a CONNECT request was reset or abnormally closed. + */ + H3_CONNECT_ERROR(0x10f), + + /** + * The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1. + */ + H3_VERSION_FALLBACK(0x110), + + /** + * The decoder failed to interpret an encoded field section and is not able to continue decoding that field section. + */ + QPACK_DECOMPRESSION_FAILED(0x200), + + /** + * The decoder failed to interpret an encoder instruction received on the encoder stream. + */ + QPACK_ENCODER_STREAM_ERROR(0x201), + + /** + * The encoder failed to interpret a decoder instruction received on the decoder stream. + */ + QPACK_DECODER_STREAM_ERROR(0x202); + + final int code; + + Http3ErrorCode(int code) { + this.code = code; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Exception.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Exception.java new file mode 100644 index 0000000..e46f7c7 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Exception.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.internal.ObjectUtil; + +/** + * An exception related to violate the HTTP3 spec. + */ +public final class Http3Exception extends Exception { + private final Http3ErrorCode errorCode; + + /** + * Create a new instance. + * + * @param errorCode the {@link Http3ErrorCode} that caused this exception. + * @param message the message to include. + */ + public Http3Exception(Http3ErrorCode errorCode, String message) { + super(message); + this.errorCode = ObjectUtil.checkNotNull(errorCode, "errorCode"); + } + + /** + * Create a new instance. + * + * @param errorCode the {@link Http3ErrorCode} that caused this exception. + * @param message the message to include. + * @param cause the {@link Throwable} to wrap. + */ + public Http3Exception(Http3ErrorCode errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = ObjectUtil.checkNotNull(errorCode, "errorCode"); + } + + /** + * Returns the related {@link Http3ErrorCode}. + * + * @return the {@link Http3ErrorCode}. + */ + public Http3ErrorCode errorCode() { + return errorCode; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Frame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Frame.java new file mode 100644 index 0000000..be406df --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Frame.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * Marker interface that is implemented by all HTTP3 frames. + */ +public interface Http3Frame { + /** + * The type of the frame. + * + * @return the type. + */ + long type(); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameCodec.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameCodec.java new file mode 100644 index 0000000..1217019 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameCodec.java @@ -0,0 +1,811 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamFrame; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; + +import java.net.SocketAddress; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_MAX_LEN; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_DATA_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_GO_AWAY_FRAME_MAX_LEN; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_HEADERS_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_MAX_LEN; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_SETTINGS_FRAME_MAX_LEN; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.numBytesForVariableLengthInteger; +import static io.netty.handler.codec.http3.Http3CodecUtils.readVariableLengthInteger; +import static io.netty.handler.codec.http3.Http3CodecUtils.writeVariableLengthInteger; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; + +/** + * Decodes / encodes {@link Http3Frame}s. + */ +final class Http3FrameCodec extends ByteToMessageDecoder implements ChannelOutboundHandler { + private final Http3FrameTypeValidator validator; + private final long maxHeaderListSize; + private final QpackDecoder qpackDecoder; + private final QpackEncoder qpackEncoder; + private final Http3RequestStreamCodecState encodeState; + private final Http3RequestStreamCodecState decodeState; + + private boolean firstFrame = true; + private boolean error; + private long type = -1; + private int payLoadLength = -1; + private QpackAttributes qpackAttributes; + private ReadResumptionListener readResumptionListener; + private WriteResumptionListener writeResumptionListener; + + static Http3FrameCodecFactory newFactory(QpackDecoder qpackDecoder, + long maxHeaderListSize, QpackEncoder qpackEncoder) { + checkNotNull(qpackEncoder, "qpackEncoder"); + checkNotNull(qpackDecoder, "qpackDecoder"); + + // QPACK decoder and encoder are shared between streams in a connection. + return (validator, encodeState, decodeState) -> new Http3FrameCodec(validator, qpackDecoder, + maxHeaderListSize, qpackEncoder, encodeState, decodeState); + } + + Http3FrameCodec(Http3FrameTypeValidator validator, QpackDecoder qpackDecoder, + long maxHeaderListSize, QpackEncoder qpackEncoder, Http3RequestStreamCodecState encodeState, + Http3RequestStreamCodecState decodeState) { + this.validator = checkNotNull(validator, "validator"); + this.qpackDecoder = checkNotNull(qpackDecoder, "qpackDecoder"); + this.maxHeaderListSize = checkPositive(maxHeaderListSize, "maxHeaderListSize"); + this.qpackEncoder = checkNotNull(qpackEncoder, "qpackEncoder"); + this.encodeState = checkNotNull(encodeState, "encodeState"); + this.decodeState = checkNotNull(decodeState, "decodeState"); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + qpackAttributes = Http3.getQpackAttributes(ctx.channel().parent()); + assert qpackAttributes != null; + + initReadResumptionListenerIfRequired(ctx); + super.handlerAdded(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + if (writeResumptionListener != null) { + writeResumptionListener.drain(); + } + super.channelInactive(ctx); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf buffer; + if (msg instanceof QuicStreamFrame) { + QuicStreamFrame streamFrame = (QuicStreamFrame) msg; + buffer = streamFrame.content().retain(); + streamFrame.release(); + } else { + buffer = (ByteBuf) msg; + } + super.channelRead(ctx, buffer); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + assert readResumptionListener != null; + if (readResumptionListener.readCompleted()) { + super.channelReadComplete(ctx); + } + } + + private void connectionError(ChannelHandlerContext ctx, Http3ErrorCode code, String msg, boolean fireException) { + error = true; + Http3CodecUtils.connectionError(ctx, code, msg, fireException); + } + + private void connectionError(ChannelHandlerContext ctx, Http3Exception exception, boolean fireException) { + error = true; + Http3CodecUtils.connectionError(ctx, exception, fireException); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + assert readResumptionListener != null; + if (!in.isReadable() || readResumptionListener.isSuspended()) { + return; + } + if (error) { + in.skipBytes(in.readableBytes()); + return; + } + if (type == -1) { + int typeLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + if (in.readableBytes() < typeLen) { + return; + } + long localType = readVariableLengthInteger(in, typeLen); + if (Http3CodecUtils.isReservedHttp2FrameType(localType)) { + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8 + connectionError(ctx, Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Reserved type for HTTP/2 received.", true); + return; + } + try { + // Validate if the type is valid for the current stream first. + validator.validate(localType, firstFrame); + } catch (Http3Exception e) { + connectionError(ctx, e, true); + return; + } + type = localType; + firstFrame = false; + if (!in.isReadable()) { + return; + } + } + if (payLoadLength == -1) { + int payloadLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + assert payloadLen <= 8; + if (in.readableBytes() < payloadLen) { + return; + } + long len = readVariableLengthInteger(in, payloadLen); + if (len > Integer.MAX_VALUE) { + connectionError(ctx, Http3ErrorCode.H3_EXCESSIVE_LOAD, + "Received an invalid frame len.", true); + return; + } + payLoadLength = (int) len; + } + int read = decodeFrame(ctx, type, payLoadLength, in, out); + if (read >= 0) { + if (read == payLoadLength) { + type = -1; + payLoadLength = -1; + } else { + payLoadLength -= read; + } + } + } + + private static int skipBytes(ByteBuf in, int payLoadLength) { + in.skipBytes(payLoadLength); + return payLoadLength; + } + + private int decodeFrame(ChannelHandlerContext ctx, long longType, int payLoadLength, ByteBuf in, List out) { + if (longType > Integer.MAX_VALUE && !Http3CodecUtils.isReservedFrameType(longType)) { + return skipBytes(in, payLoadLength); + } + int type = (int) longType; + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-11.2.1 + switch (type) { + case HTTP3_DATA_FRAME_TYPE: + // DATA + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.1 + int readable = in.readableBytes(); + if (readable == 0 && payLoadLength > 0) { + return 0; + } + int length = Math.min(readable, payLoadLength); + out.add(new DefaultHttp3DataFrame(in.readRetainedSlice(length))); + return length; + case HTTP3_HEADERS_FRAME_TYPE: + // HEADERS + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.2 + if (!enforceMaxPayloadLength(ctx, in, type, payLoadLength, + // Let's use the maxHeaderListSize as a limit as this is this is the decompressed amounts of + // bytes which means the once we decompressed the headers we will be bigger then the actual + // payload size now. + maxHeaderListSize, Http3ErrorCode.H3_EXCESSIVE_LOAD)) { + return 0; + } + assert qpackAttributes != null; + if (!qpackAttributes.dynamicTableDisabled() && !qpackAttributes.decoderStreamAvailable()) { + assert readResumptionListener != null; + readResumptionListener.suspended(); + return 0; + } + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + if (decodeHeaders(ctx, headersFrame.headers(), in, payLoadLength, decodeState.receivedFinalHeaders())) { + out.add(headersFrame); + return payLoadLength; + } + return -1; + case HTTP3_CANCEL_PUSH_FRAME_TYPE: + // CANCEL_PUSH + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.3 + if (!enforceMaxPayloadLength(ctx, in, type, payLoadLength, + HTTP3_CANCEL_PUSH_FRAME_MAX_LEN, Http3ErrorCode.H3_FRAME_ERROR)) { + return 0; + } + int pushIdLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + out.add(new DefaultHttp3CancelPushFrame(readVariableLengthInteger(in, pushIdLen))); + return payLoadLength; + case HTTP3_SETTINGS_FRAME_TYPE: + // SETTINGS + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.4 + + // Use 256 as this gives space for 16 maximal size encoder and 128 minimal size encoded settings. + if (!enforceMaxPayloadLength(ctx, in, type, payLoadLength, HTTP3_SETTINGS_FRAME_MAX_LEN, + Http3ErrorCode.H3_EXCESSIVE_LOAD)) { + return 0; + } + Http3SettingsFrame settingsFrame = decodeSettings(ctx, in, payLoadLength); + if (settingsFrame != null) { + out.add(settingsFrame); + } + return payLoadLength; + case HTTP3_PUSH_PROMISE_FRAME_TYPE: + // PUSH_PROMISE + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.5 + if (!enforceMaxPayloadLength(ctx, in, type, payLoadLength, + // Let's use the maxHeaderListSize as a limit as this is this is the decompressed amounts of + // bytes which means the once we decompressed the headers we will be bigger then the actual + // payload size now. + Math.max(maxHeaderListSize, maxHeaderListSize + 8), Http3ErrorCode.H3_EXCESSIVE_LOAD)) { + return 0; + } + + assert qpackAttributes != null; + if (!qpackAttributes.dynamicTableDisabled() && !qpackAttributes.decoderStreamAvailable()) { + assert readResumptionListener != null; + readResumptionListener.suspended(); + return 0; + } + int readerIdx = in.readerIndex(); + int pushPromiseIdLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + Http3PushPromiseFrame pushPromiseFrame = new DefaultHttp3PushPromiseFrame( + readVariableLengthInteger(in, pushPromiseIdLen)); + if (decodeHeaders(ctx, pushPromiseFrame.headers(), in, payLoadLength - pushPromiseIdLen, false)) { + out.add(pushPromiseFrame); + return payLoadLength; + } + in.readerIndex(readerIdx); + return -1; + case HTTP3_GO_AWAY_FRAME_TYPE: + // GO_AWAY + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.6 + if (!enforceMaxPayloadLength(ctx, in, type, payLoadLength, + HTTP3_GO_AWAY_FRAME_MAX_LEN, Http3ErrorCode.H3_FRAME_ERROR)) { + return 0; + } + int idLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + out.add(new DefaultHttp3GoAwayFrame(readVariableLengthInteger(in, idLen))); + return payLoadLength; + case HTTP3_MAX_PUSH_ID_FRAME_TYPE: + // MAX_PUSH_ID + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.7 + if (!enforceMaxPayloadLength(ctx, in, type, payLoadLength, + HTTP3_MAX_PUSH_ID_FRAME_MAX_LEN, Http3ErrorCode.H3_FRAME_ERROR)) { + return 0; + } + int pidLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + out.add(new DefaultHttp3MaxPushIdFrame(readVariableLengthInteger(in, pidLen))); + return payLoadLength; + default: + if (!Http3CodecUtils.isReservedFrameType(longType)) { + return skipBytes(in, payLoadLength); + } + // Handling reserved frame types + // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8 + if (in.readableBytes() < payLoadLength) { + return 0; + } + out.add(new DefaultHttp3UnknownFrame(longType, in.readRetainedSlice(payLoadLength))); + return payLoadLength; + } + } + + private boolean enforceMaxPayloadLength( + ChannelHandlerContext ctx, ByteBuf in, int type, int payLoadLength, + long maxPayLoadLength, Http3ErrorCode error) { + if (payLoadLength > maxPayLoadLength) { + connectionError(ctx, error, + "Received an invalid frame len " + payLoadLength + " for frame of type " + type + '.', true); + return false; + } + return in.readableBytes() >= payLoadLength; + } + + private Http3SettingsFrame decodeSettings(ChannelHandlerContext ctx, ByteBuf in, int payLoadLength) { + Http3SettingsFrame settingsFrame = new DefaultHttp3SettingsFrame(); + while (payLoadLength > 0) { + int keyLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + long key = readVariableLengthInteger(in, keyLen); + if (Http3CodecUtils.isReservedHttp2Setting(key)) { + // This must be treated as a connection error + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.4.1 + connectionError(ctx, Http3ErrorCode.H3_SETTINGS_ERROR, + "Received a settings key that is reserved for HTTP/2.", true); + return null; + } + payLoadLength -= keyLen; + int valueLen = numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + long value = readVariableLengthInteger(in, valueLen); + payLoadLength -= valueLen; + + if (settingsFrame.put(key, value) != null) { + // This must be treated as a connection error + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.4 + connectionError(ctx, Http3ErrorCode.H3_SETTINGS_ERROR, + "Received a duplicate settings key.", true); + return null; + } + } + return settingsFrame; + } + + /** + * Decode the header block into header fields. + * + * @param ctx {@link ChannelHandlerContext} for this handler. + * @param headers to be populated by decode. + * @param in {@link ByteBuf} containing the encode header block. It is assumed that the entire header block is + * contained in this buffer. + * @param length Number of bytes in the passed buffer that represent the encoded header block. + * @param trailer {@code true} if this is a trailer section. + * @return {@code true} if the headers were decoded, {@code false} otherwise. A header block may not be decoded if + * it is awaiting QPACK dynamic table updates. + */ + private boolean decodeHeaders(ChannelHandlerContext ctx, Http3Headers headers, ByteBuf in, int length, + boolean trailer) { + try { + Http3HeadersSink sink = new Http3HeadersSink(headers, maxHeaderListSize, true, trailer); + assert qpackAttributes != null; + assert readResumptionListener != null; + if (qpackDecoder.decode(qpackAttributes, + ((QuicStreamChannel) ctx.channel()).streamId(), in, length, sink, readResumptionListener)) { + // Throws exception if detected any problem so far + sink.finish(); + return true; + } + readResumptionListener.suspended(); + } catch (Http3Exception e) { + connectionError(ctx, e.errorCode(), e.getMessage(), true); + } catch (QpackException e) { + // Must be treated as a connection error. + connectionError(ctx, Http3ErrorCode.QPACK_DECOMPRESSION_FAILED, + "Decompression of header block failed.", true); + } catch (Http3HeadersValidationException e) { + error = true; + ctx.fireExceptionCaught(e); + // We should shutdown the stream with an error. + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-4.1.3 + Http3CodecUtils.streamError(ctx, Http3ErrorCode.H3_MESSAGE_ERROR); + } + return false; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + assert qpackAttributes != null; + if (writeResumptionListener != null) { + writeResumptionListener.enqueue(msg, promise); + return; + } + + if ((msg instanceof Http3HeadersFrame || msg instanceof Http3PushPromiseFrame) && + !qpackAttributes.dynamicTableDisabled() && !qpackAttributes.encoderStreamAvailable()) { + writeResumptionListener = WriteResumptionListener.newListener(ctx, this); + writeResumptionListener.enqueue(msg, promise); + return; + } + + write0(ctx, msg, promise); + } + + private void write0(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + try { + if (msg instanceof Http3DataFrame) { + writeDataFrame(ctx, (Http3DataFrame) msg, promise); + } else if (msg instanceof Http3HeadersFrame) { + writeHeadersFrame(ctx, (Http3HeadersFrame) msg, promise); + } else if (msg instanceof Http3CancelPushFrame) { + writeCancelPushFrame(ctx, (Http3CancelPushFrame) msg, promise); + } else if (msg instanceof Http3SettingsFrame) { + writeSettingsFrame(ctx, (Http3SettingsFrame) msg, promise); + } else if (msg instanceof Http3PushPromiseFrame) { + writePushPromiseFrame(ctx, (Http3PushPromiseFrame) msg, promise); + } else if (msg instanceof Http3GoAwayFrame) { + writeGoAwayFrame(ctx, (Http3GoAwayFrame) msg, promise); + } else if (msg instanceof Http3MaxPushIdFrame) { + writeMaxPushIdFrame(ctx, (Http3MaxPushIdFrame) msg, promise); + } else if (msg instanceof Http3UnknownFrame) { + writeUnknownFrame(ctx, (Http3UnknownFrame) msg, promise); + } else { + unsupported(promise); + } + } finally { + ReferenceCountUtil.release(msg); + } + } + + private static void writeDataFrame( + ChannelHandlerContext ctx, Http3DataFrame frame, ChannelPromise promise) { + ByteBuf out = ctx.alloc().directBuffer(16); + writeVariableLengthInteger(out, frame.type()); + writeVariableLengthInteger(out, frame.content().readableBytes()); + ByteBuf content = frame.content().retain(); + ctx.write(Unpooled.wrappedUnmodifiableBuffer(out, content), promise); + } + + private void writeHeadersFrame(ChannelHandlerContext ctx, Http3HeadersFrame frame, ChannelPromise promise) { + assert qpackAttributes != null; + final QuicStreamChannel channel = (QuicStreamChannel) ctx.channel(); + writeDynamicFrame(ctx, frame.type(), frame, (f, out) -> { + qpackEncoder.encodeHeaders(qpackAttributes, out, ctx.alloc(), channel.streamId(), f.headers()); + return true; + }, promise); + } + + private static void writeCancelPushFrame( + ChannelHandlerContext ctx, Http3CancelPushFrame frame, ChannelPromise promise) { + writeFrameWithId(ctx, frame.type(), frame.id(), promise); + } + + private static void writeSettingsFrame( + ChannelHandlerContext ctx, Http3SettingsFrame frame, ChannelPromise promise) { + writeDynamicFrame(ctx, frame.type(), frame, (f, out) -> { + for (Map.Entry e : f) { + Long key = e.getKey(); + if (Http3CodecUtils.isReservedHttp2Setting(key)) { + Http3Exception exception = new Http3Exception(Http3ErrorCode.H3_SETTINGS_ERROR, + "Received a settings key that is reserved for HTTP/2."); + promise.setFailure(exception); + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8 + Http3CodecUtils.connectionError(ctx, exception, false); + return false; + } + Long value = e.getValue(); + int keyLen = numBytesForVariableLengthInteger(key); + int valueLen = numBytesForVariableLengthInteger(value); + writeVariableLengthInteger(out, key, keyLen); + writeVariableLengthInteger(out, value, valueLen); + } + return true; + }, promise); + } + + private static void writeDynamicFrame(ChannelHandlerContext ctx, long type, T frame, + BiFunction writer, + ChannelPromise promise) { + ByteBuf out = ctx.alloc().directBuffer(); + int initialWriterIndex = out.writerIndex(); + // Move 16 bytes forward as this is the maximum amount we could ever need for the type + payload length. + int payloadStartIndex = initialWriterIndex + 16; + out.writerIndex(payloadStartIndex); + + if (writer.apply(frame, out)) { + int finalWriterIndex = out.writerIndex(); + int payloadLength = finalWriterIndex - payloadStartIndex; + int len = numBytesForVariableLengthInteger(payloadLength); + out.writerIndex(payloadStartIndex - len); + writeVariableLengthInteger(out, payloadLength, len); + + int typeLength = numBytesForVariableLengthInteger(type); + int startIndex = payloadStartIndex - len - typeLength; + out.writerIndex(startIndex); + writeVariableLengthInteger(out, type, typeLength); + + out.setIndex(startIndex, finalWriterIndex); + ctx.write(out, promise); + } else { + // We failed to encode, lets release the buffer so we dont leak. + out.release(); + } + } + + private void writePushPromiseFrame(ChannelHandlerContext ctx, Http3PushPromiseFrame frame, ChannelPromise promise) { + assert qpackAttributes != null; + final QuicStreamChannel channel = (QuicStreamChannel) ctx.channel(); + writeDynamicFrame(ctx, frame.type(), frame, (f, out) -> { + long id = f.id(); + writeVariableLengthInteger(out, id); + qpackEncoder.encodeHeaders(qpackAttributes, out, ctx.alloc(), channel.streamId(), f.headers()); + return true; + }, promise); + } + + private static void writeGoAwayFrame( + ChannelHandlerContext ctx, Http3GoAwayFrame frame, ChannelPromise promise) { + writeFrameWithId(ctx, frame.type(), frame.id(), promise); + } + + private static void writeMaxPushIdFrame( + ChannelHandlerContext ctx, Http3MaxPushIdFrame frame, ChannelPromise promise) { + writeFrameWithId(ctx, frame.type(), frame.id(), promise); + } + + private static void writeFrameWithId(ChannelHandlerContext ctx, long type, long id, ChannelPromise promise) { + ByteBuf out = ctx.alloc().directBuffer(24); + writeVariableLengthInteger(out, type); + writeVariableLengthInteger(out, numBytesForVariableLengthInteger(id)); + writeVariableLengthInteger(out, id); + ctx.write(out, promise); + } + + private void writeUnknownFrame( + ChannelHandlerContext ctx, Http3UnknownFrame frame, ChannelPromise promise) { + long type = frame.type(); + if (Http3CodecUtils.isReservedHttp2FrameType(type)) { + Http3Exception exception = new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Reserved type for HTTP/2 send."); + promise.setFailure(exception); + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8 + connectionError(ctx, exception.errorCode(), exception.getMessage(), false); + return; + } + if (!Http3CodecUtils.isReservedFrameType(type)) { + Http3Exception exception = new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Non reserved type for HTTP/3 send."); + promise.setFailure(exception); + return; + } + ByteBuf out = ctx.alloc().directBuffer(); + writeVariableLengthInteger(out, type); + writeVariableLengthInteger(out, frame.content().readableBytes()); + ByteBuf content = frame.content().retain(); + ctx.write(Unpooled.wrappedUnmodifiableBuffer(out, content), promise); + } + + private void initReadResumptionListenerIfRequired(ChannelHandlerContext ctx) { + if (readResumptionListener == null) { + readResumptionListener = new ReadResumptionListener(ctx, this); + } + } + + private static void unsupported(ChannelPromise promise) { + promise.setFailure(new UnsupportedOperationException()); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) { + assert readResumptionListener != null; + if (readResumptionListener.readRequested()) { + ctx.read(); + } + } + + @Override + public void flush(ChannelHandlerContext ctx) { + if (writeResumptionListener != null) { + writeResumptionListener.enqueueFlush(); + } else { + ctx.flush(); + } + } + + private static final class ReadResumptionListener + implements Runnable, GenericFutureListener> { + private static final int STATE_SUSPENDED = 0b1000_0000; + private static final int STATE_READ_PENDING = 0b0100_0000; + private static final int STATE_READ_COMPLETE_PENDING = 0b0010_0000; + + private final ChannelHandlerContext ctx; + private final Http3FrameCodec codec; + private byte state; + + ReadResumptionListener(ChannelHandlerContext ctx, Http3FrameCodec codec) { + this.ctx = ctx; + this.codec = codec; + assert codec.qpackAttributes != null; + if (!codec.qpackAttributes.dynamicTableDisabled() && !codec.qpackAttributes.decoderStreamAvailable()) { + codec.qpackAttributes.whenDecoderStreamAvailable(this); + } + } + + void suspended() { + assert !codec.qpackAttributes.dynamicTableDisabled(); + setState(STATE_SUSPENDED); + } + + boolean readCompleted() { + if (hasState(STATE_SUSPENDED)) { + setState(STATE_READ_COMPLETE_PENDING); + return false; + } + return true; + } + + boolean readRequested() { + if (hasState(STATE_SUSPENDED)) { + setState(STATE_READ_PENDING); + return false; + } + return true; + } + + boolean isSuspended() { + return hasState(STATE_SUSPENDED); + } + + @Override + public void operationComplete(Future future) { + if (future.isSuccess()) { + resume(); + } else { + ctx.fireExceptionCaught(future.cause()); + } + } + + @Override + public void run() { + resume(); + } + + private void resume() { + unsetState(STATE_SUSPENDED); + try { + codec.channelRead(ctx, Unpooled.EMPTY_BUFFER); + if (hasState(STATE_READ_COMPLETE_PENDING)) { + unsetState(STATE_READ_COMPLETE_PENDING); + codec.channelReadComplete(ctx); + } + if (hasState(STATE_READ_PENDING)) { + unsetState(STATE_READ_PENDING); + codec.read(ctx); + } + } catch (Exception e) { + ctx.fireExceptionCaught(e); + } + } + + private void setState(int toSet) { + state |= toSet; + } + + private boolean hasState(int toCheck) { + return (state & toCheck) == toCheck; + } + + private void unsetState(int toUnset) { + state &= ~toUnset; + } + } + + private static final class WriteResumptionListener + implements GenericFutureListener> { + private static final Object FLUSH = new Object(); + private final Deque buffer; + private final ChannelHandlerContext ctx; + private final Http3FrameCodec codec; + + private WriteResumptionListener(ChannelHandlerContext ctx, Http3FrameCodec codec) { + this.ctx = ctx; + this.codec = codec; + buffer = new ArrayDeque<>(4); // assuming we will buffer header, data, trailer and a flush + } + + @Override + public void operationComplete(Future future) { + drain(); + } + + void enqueue(Object msg, ChannelPromise promise) { + assert ctx.channel().eventLoop().inEventLoop(); + buffer.addLast(new BufferedEntry(msg, promise)); + } + + void enqueueFlush() { + assert ctx.channel().eventLoop().inEventLoop(); + buffer.addLast(FLUSH); + } + + void drain() { + assert ctx.channel().eventLoop().inEventLoop(); + boolean flushSeen = false; + try { + for (Object entry = buffer.pollFirst(); entry != null; entry = buffer.pollFirst()) { + if (entry == FLUSH) { + flushSeen = true; + } else { + assert entry instanceof BufferedEntry; + BufferedEntry bufferedEntry = (BufferedEntry) entry; + codec.write0(ctx, bufferedEntry.msg, bufferedEntry.promise); + } + } + // indicate that writes do not need to be enqueued. As we are on the eventloop, no other writes can + // happen while we are draining, hence we would not write out of order. + codec.writeResumptionListener = null; + } finally { + if (flushSeen) { + codec.flush(ctx); + } + } + } + + static WriteResumptionListener newListener(ChannelHandlerContext ctx, Http3FrameCodec codec) { + WriteResumptionListener listener = new WriteResumptionListener(ctx, codec); + assert codec.qpackAttributes != null; + codec.qpackAttributes.whenEncoderStreamAvailable(listener); + return listener; + } + + private static final class BufferedEntry { + private final Object msg; + private final ChannelPromise promise; + + BufferedEntry(Object msg, ChannelPromise promise) { + this.msg = msg; + this.promise = promise; + } + } + } + + /** + * A factory for creating codec for HTTP3 frames. + */ + @FunctionalInterface + interface Http3FrameCodecFactory { + /** + * Creates a new codec instance for the passed {@code streamType}. + * + * @param validator for the frames. + * @param encodeState for the request stream. + * @param decodeState for the request stream. + * @return new codec instance for the passed {@code streamType}. + */ + ChannelHandler newCodec(Http3FrameTypeValidator validator, Http3RequestStreamCodecState encodeState, + Http3RequestStreamCodecState decodeState); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameToHttpObjectCodec.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameToHttpObjectCodec.java new file mode 100644 index 0000000..cbf2bb5 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameToHttpObjectCodec.java @@ -0,0 +1,293 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.EncoderException; +import io.netty.handler.codec.UnsupportedMessageTypeException; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpScheme; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.concurrent.PromiseCombiner; + +import java.net.SocketAddress; + +/** + * This handler converts from {@link Http3RequestStreamFrame} to {@link HttpObject}, + * and back. It can be used as an adapter in conjunction with {@link + * Http3ServerConnectionHandler} or {@link Http3ClientConnectionHandler} to make http/3 connections + * backward-compatible with {@link ChannelHandler}s expecting {@link HttpObject}. + * + * For simplicity, it converts to chunked encoding unless the entire stream + * is a single header. + */ +public final class Http3FrameToHttpObjectCodec extends Http3RequestStreamInboundHandler + implements ChannelOutboundHandler { + + private final boolean isServer; + private final boolean validateHeaders; + private boolean inboundTranslationInProgress; + + public Http3FrameToHttpObjectCodec(final boolean isServer, + final boolean validateHeaders) { + this.isServer = isServer; + this.validateHeaders = validateHeaders; + } + + public Http3FrameToHttpObjectCodec(final boolean isServer) { + this(isServer, true); + } + + @Override + public boolean isSharable() { + return false; + } + + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3HeadersFrame frame) throws Exception { + Http3Headers headers = frame.headers(); + long id = ((QuicStreamChannel) ctx.channel()).streamId(); + + final CharSequence status = headers.status(); + + // 100-continue response is a special case where we should not send a fin, + // but we need to decode it as a FullHttpResponse to play nice with HttpObjectAggregator. + if (null != status && HttpResponseStatus.CONTINUE.codeAsText().contentEquals(status)) { + final FullHttpMessage fullMsg = newFullMessage(id, headers, ctx.alloc()); + ctx.fireChannelRead(fullMsg); + return; + } + + if (headers.method() == null && status == null) { + // Must be trailers! + LastHttpContent last = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders); + HttpConversionUtil.addHttp3ToHttpHeaders(id, headers, last.trailingHeaders(), + HttpVersion.HTTP_1_1, true, true); + inboundTranslationInProgress = false; + ctx.fireChannelRead(last); + } else { + HttpMessage req = newMessage(id, headers); + if (!HttpUtil.isContentLengthSet(req)) { + req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + } + inboundTranslationInProgress = true; + ctx.fireChannelRead(req); + } + } + + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3DataFrame frame) throws Exception { + inboundTranslationInProgress = true; + ctx.fireChannelRead(new DefaultHttpContent(frame.content())); + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) throws Exception { + if (inboundTranslationInProgress) { + ctx.fireChannelRead(LastHttpContent.EMPTY_LAST_CONTENT); + } + } + + /** + * Encode from an {@link HttpObject} to an {@link Http3RequestStreamFrame}. This method will + * be called for each written message that can be handled by this encoder. + * + * NOTE: 100-Continue responses that are NOT {@link FullHttpResponse} will be rejected. + * + * @param ctx the {@link ChannelHandlerContext} which this handler belongs to + * @param msg the {@link HttpObject} message to encode + * @throws Exception is thrown if an error occurs + */ + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof HttpObject)) { + throw new UnsupportedMessageTypeException(); + } + // 100-continue is typically a FullHttpResponse, but the decoded + // Http3HeadersFrame should not handles as a end of stream. + if (msg instanceof HttpResponse) { + final HttpResponse res = (HttpResponse) msg; + if (res.status().equals(HttpResponseStatus.CONTINUE)) { + if (res instanceof FullHttpResponse) { + final Http3Headers headers = toHttp3Headers(res); + ctx.write(new DefaultHttp3HeadersFrame(headers), promise); + ((FullHttpResponse) res).release(); + return; + } else { + throw new EncoderException( + HttpResponseStatus.CONTINUE.toString() + " must be a FullHttpResponse"); + } + } + } + + // this combiner is created lazily if we need multiple write calls + PromiseCombiner combiner = null; + // With the last content, *if* we write anything here, we need to wait for that write to complete before + // closing. To do that, we need to unvoid the promise. So if we write anything *and* this is the last message + // we will unvoid. + boolean isLast = msg instanceof LastHttpContent; + + if (msg instanceof HttpMessage) { + Http3Headers headers = toHttp3Headers((HttpMessage) msg); + DefaultHttp3HeadersFrame frame = new DefaultHttp3HeadersFrame(headers); + + if (msg instanceof HttpContent && (!promise.isVoid() || isLast)) { + combiner = new PromiseCombiner(ctx.executor()); + } + promise = writeWithOptionalCombiner(ctx, frame, promise, combiner, isLast); + } + + if (isLast) { + LastHttpContent last = (LastHttpContent) msg; + boolean readable = last.content().isReadable(); + boolean hasTrailers = !last.trailingHeaders().isEmpty(); + + if (combiner == null && readable && hasTrailers && !promise.isVoid()) { + combiner = new PromiseCombiner(ctx.executor()); + } + + if (readable) { + promise = writeWithOptionalCombiner(ctx, + new DefaultHttp3DataFrame(last.content()), promise, combiner, true); + } + if (hasTrailers) { + Http3Headers headers = HttpConversionUtil.toHttp3Headers(last.trailingHeaders(), validateHeaders); + promise = writeWithOptionalCombiner(ctx, + new DefaultHttp3HeadersFrame(headers), promise, combiner, true); + } + if (!readable) { + last.release(); + } + + if (!readable && !hasTrailers && combiner == null) { + // we had to write nothing. happy days! + ((QuicStreamChannel) ctx.channel()).shutdownOutput(); + promise.trySuccess(); + } else { + promise.addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + } else if (msg instanceof HttpContent) { + promise = writeWithOptionalCombiner(ctx, + new DefaultHttp3DataFrame(((HttpContent) msg).content()), promise, combiner, false); + } + + if (combiner != null) { + combiner.finish(promise); + } + } + + /** + * Write a message. If there is a combiner, add a new write promise to that combiner. If there is no combiner + * ({@code null}), use the {@code outerPromise} directly as the write promise. + */ + private static ChannelPromise writeWithOptionalCombiner( + ChannelHandlerContext ctx, + Object msg, + ChannelPromise outerPromise, + PromiseCombiner combiner, + boolean unvoidPromise + ) { + if (unvoidPromise) { + outerPromise = outerPromise.unvoid(); + } + if (combiner == null) { + ctx.write(msg, outerPromise); + } else { + combiner.add(ctx.write(msg)); + } + return outerPromise; + } + + private Http3Headers toHttp3Headers(HttpMessage msg) { + if (msg instanceof HttpRequest) { + msg.headers().set( + HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), HttpScheme.HTTPS); + } + + return HttpConversionUtil.toHttp3Headers(msg, validateHeaders); + } + + private HttpMessage newMessage(final long id, + final Http3Headers headers) throws Http3Exception { + return isServer ? + HttpConversionUtil.toHttpRequest(id, headers, validateHeaders) : + HttpConversionUtil.toHttpResponse(id, headers, validateHeaders); + } + + private FullHttpMessage newFullMessage(final long id, + final Http3Headers headers, + final ByteBufAllocator alloc) throws Http3Exception { + return isServer ? + HttpConversionUtil.toFullHttpRequest(id, headers, alloc, validateHeaders) : + HttpConversionUtil.toFullHttpResponse(id, headers, alloc, validateHeaders); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + ctx.read(); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeDuplexValidationHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeDuplexValidationHandler.java new file mode 100644 index 0000000..30d9485 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeDuplexValidationHandler.java @@ -0,0 +1,87 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; + +import java.net.SocketAddress; + +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.frameTypeUnexpected; +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.validateFrameWritten; + +class Http3FrameTypeDuplexValidationHandler extends Http3FrameTypeInboundValidationHandler + implements ChannelOutboundHandler { + + Http3FrameTypeDuplexValidationHandler(Class frameType) { + super(frameType); + } + + @Override + public final void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + T frame = validateFrameWritten(frameType, msg); + if (frame != null) { + write(ctx, frame, promise); + } else { + writeFrameDiscarded(msg, promise); + } + } + + void write(ChannelHandlerContext ctx, T msg, ChannelPromise promise) { + ctx.write(msg, promise); + } + + void writeFrameDiscarded(Object discardedFrame, ChannelPromise promise) { + frameTypeUnexpected(promise, discardedFrame); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) throws Exception { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + ctx.read(); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeInboundValidationHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeInboundValidationHandler.java new file mode 100644 index 0000000..658d131 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeInboundValidationHandler.java @@ -0,0 +1,50 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.internal.ObjectUtil; + +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.frameTypeUnexpected; +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.validateFrameRead; + +class Http3FrameTypeInboundValidationHandler extends ChannelInboundHandlerAdapter { + + protected final Class frameType; + + Http3FrameTypeInboundValidationHandler(Class frameType) { + this.frameType = ObjectUtil.checkNotNull(frameType, "frameType"); + } + + @Override + public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + final T frame = validateFrameRead(frameType, msg); + if (frame != null) { + channelRead(ctx, frame); + } else { + readFrameDiscarded(ctx, msg); + } + } + + void channelRead(ChannelHandlerContext ctx, T frame) throws Exception { + ctx.fireChannelRead(frame); + } + + void readFrameDiscarded(ChannelHandlerContext ctx, Object discardedFrame) { + frameTypeUnexpected(ctx, discardedFrame); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeOutboundValidationHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeOutboundValidationHandler.java new file mode 100644 index 0000000..42265b3 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeOutboundValidationHandler.java @@ -0,0 +1,51 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ObjectUtil; + +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.frameTypeUnexpected; +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.validateFrameWritten; + +class Http3FrameTypeOutboundValidationHandler extends ChannelOutboundHandlerAdapter { + + private final Class frameType; + + Http3FrameTypeOutboundValidationHandler(Class frameType) { + this.frameType = ObjectUtil.checkNotNull(frameType, "frameType"); + } + + @Override + public final void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + T frame = validateFrameWritten(frameType, msg); + if (frame != null) { + write(ctx, frame, promise); + } else { + writeFrameDiscarded(msg, promise); + } + } + + void write(ChannelHandlerContext ctx, T msg, ChannelPromise promise) { + ctx.write(msg, promise); + } + + void writeFrameDiscarded(Object discardedFrame, ChannelPromise promise) { + frameTypeUnexpected(promise, discardedFrame); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeValidator.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeValidator.java new file mode 100644 index 0000000..a597115 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameTypeValidator.java @@ -0,0 +1,24 @@ +/* + * 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.handler.codec.http3; + +@FunctionalInterface +interface Http3FrameTypeValidator { + + Http3FrameTypeValidator NO_VALIDATION = (type, first) -> { }; + + void validate(long type, boolean first) throws Http3Exception; +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameValidationUtils.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameValidationUtils.java new file mode 100644 index 0000000..751cc53 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3FrameValidationUtils.java @@ -0,0 +1,94 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.StringUtil; + +final class Http3FrameValidationUtils { + + private Http3FrameValidationUtils() { + // no instances + } + + @SuppressWarnings("unchecked") + private static T cast(Object msg) { + return (T) msg; + } + + private static boolean isValid(Class frameType, Object msg) { + return frameType.isInstance(msg); + } + + /** + * Check if the passed {@code msg} is of the {@code expectedFrameType} and return the expected type, else return + * {@code null}. + * + * @param expectedFrameType {@link Class} of the expected frame type. + * @param msg to validate. + * @param Expected type. + * @return {@code msg} as expected frame type or {@code null} if it can not be converted to the expected type. + */ + static T validateFrameWritten(Class expectedFrameType, Object msg) { + if (isValid(expectedFrameType, msg)) { + return cast(msg); + } + return null; + } + + /** + * Check if the passed {@code msg} is of the {@code expectedFrameType} and return the expected type, else return + * {@code null}. + * + * @param expectedFrameType {@link Class} of the expected frame type. + * @param msg to validate. + * @param Expected type. + * @return {@code msg} as expected frame type or {@code null} if it can not be converted to the expected type. + */ + static T validateFrameRead(Class expectedFrameType, Object msg) { + if (isValid(expectedFrameType, msg)) { + return cast(msg); + } + return null; + } + + /** + * Handle unexpected frame type by failing the passed {@link ChannelPromise}. + * + * @param promise to fail. + * @param frame which is unexpected. + */ + static void frameTypeUnexpected(ChannelPromise promise, Object frame) { + String type = StringUtil.simpleClassName(frame); + ReferenceCountUtil.release(frame); + promise.setFailure(new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Frame of type " + type + " unexpected")); + } + + /** + * Handle unexpected frame type by propagating a connection error with code: + * {@link Http3ErrorCode#H3_FRAME_UNEXPECTED}. + * + * @param ctx to use for propagation of failure. + * @param frame which is unexpected. + */ + static void frameTypeUnexpected(ChannelHandlerContext ctx, Object frame) { + ReferenceCountUtil.release(frame); + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_FRAME_UNEXPECTED, "Frame type unexpected", true); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3GoAwayFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3GoAwayFrame.java new file mode 100644 index 0000000..1a41fc6 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3GoAwayFrame.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * See GOAWAY. + */ +public interface Http3GoAwayFrame extends Http3ControlStreamFrame { + + @Override + default long type() { + return Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE; + } + + /** + * Returns the id. + * + * @return the id. + */ + long id(); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Headers.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Headers.java new file mode 100644 index 0000000..e98cb4d --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3Headers.java @@ -0,0 +1,225 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.handler.codec.Headers; +import io.netty.util.AsciiString; + +import java.util.Iterator; +import java.util.Map.Entry; + +public interface Http3Headers extends Headers { + + /** + * HTTP/2 (and HTTP/3) pseudo-headers names. + */ + enum PseudoHeaderName { + /** + * {@code :method}. + */ + METHOD(":method", true), + + /** + * {@code :scheme}. + */ + SCHEME(":scheme", true), + + /** + * {@code :authority}. + */ + AUTHORITY(":authority", true), + + /** + * {@code :path}. + */ + PATH(":path", true), + + /** + * {@code :status}. + */ + STATUS(":status", false); + + private static final char PSEUDO_HEADER_PREFIX = ':'; + private static final byte PSEUDO_HEADER_PREFIX_BYTE = (byte) PSEUDO_HEADER_PREFIX; + + private final AsciiString value; + private final boolean requestOnly; + private static final CharSequenceMap PSEUDO_HEADERS = new CharSequenceMap(); + + static { + for (PseudoHeaderName pseudoHeader : PseudoHeaderName.values()) { + PSEUDO_HEADERS.add(pseudoHeader.value(), pseudoHeader); + } + } + + PseudoHeaderName(String value, boolean requestOnly) { + this.value = AsciiString.cached(value); + this.requestOnly = requestOnly; + } + + public AsciiString value() { + // Return a slice so that the buffer gets its own reader index. + return value; + } + + /** + * Indicates whether the specified header follows the pseudo-header format (begins with ':' character) + * + * @param headerName the header name to check. + * @return {@code true} if the header follow the pseudo-header format + */ + public static boolean hasPseudoHeaderFormat(CharSequence headerName) { + if (headerName instanceof AsciiString) { + final AsciiString asciiHeaderName = (AsciiString) headerName; + return asciiHeaderName.length() > 0 && asciiHeaderName.byteAt(0) == PSEUDO_HEADER_PREFIX_BYTE; + } else { + return headerName.length() > 0 && headerName.charAt(0) == PSEUDO_HEADER_PREFIX; + } + } + + /** + * Indicates whether the given header name is a valid HTTP/3 pseudo header. + * + * @param name the header name. + * @return {@code true} if the given header name is a valid HTTP/3 pseudo header, {@code false} otherwise. + */ + public static boolean isPseudoHeader(CharSequence name) { + return PSEUDO_HEADERS.contains(name); + } + + /** + * Returns the {@link PseudoHeaderName} corresponding to the specified header name. + * + * @param name the header name. + * @return corresponding {@link PseudoHeaderName} if any, {@code null} otherwise. + */ + public static PseudoHeaderName getPseudoHeader(CharSequence name) { + return PSEUDO_HEADERS.get(name); + } + + /** + * Indicates whether the pseudo-header is to be used in a request context. + * + * @return {@code true} if the pseudo-header is to be used in a request context + */ + public boolean isRequestOnly() { + return requestOnly; + } + } + + /** + * Returns an iterator over all HTTP/3 headers. The iteration order is as follows: + * 1. All pseudo headers (order not specified). + * 2. All non-pseudo headers (in insertion order). + */ + @Override + Iterator> iterator(); + + /** + * Equivalent to {@link #getAll(Object)} but no intermediate list is generated. + * @param name the name of the header to retrieve + * @return an {@link Iterator} of header values corresponding to {@code name}. + */ + Iterator valueIterator(CharSequence name); + + /** + * Sets the {@link PseudoHeaderName#METHOD} header + * + * @param value the value for the header. + * @return this instance itself. + */ + Http3Headers method(CharSequence value); + + /** + * Sets the {@link PseudoHeaderName#SCHEME} header + * + * @param value the value for the header. + * @return this instance itself. + */ + Http3Headers scheme(CharSequence value); + + /** + * Sets the {@link PseudoHeaderName#AUTHORITY} header + * + * @param value the value for the header. + * @return this instance itself. + */ + Http3Headers authority(CharSequence value); + + /** + * Sets the {@link PseudoHeaderName#PATH} header + * + * @param value the value for the header. + * @return this instance itself. + */ + Http3Headers path(CharSequence value); + + /** + * Sets the {@link PseudoHeaderName#STATUS} header + * + * @param value the value for the header. + * @return this instance itself. + */ + Http3Headers status(CharSequence value); + + /** + * Gets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header + * + * @return the value of the header. + */ + CharSequence method(); + + /** + * Gets the {@link PseudoHeaderName#SCHEME} header or {@code null} if there is no such header + * + * @return the value of the header. + */ + CharSequence scheme(); + + /** + * Gets the {@link PseudoHeaderName#AUTHORITY} header or {@code null} if there is no such header + * + * @return the value of the header. + */ + CharSequence authority(); + + /** + * Gets the {@link PseudoHeaderName#PATH} header or {@code null} if there is no such header + * + * @return the value of the header. + */ + CharSequence path(); + + /** + * Gets the {@link PseudoHeaderName#STATUS} header or {@code null} if there is no such header + * + * @return the value of the header. + */ + CharSequence status(); + + /** + * Returns {@code true} if a header with the {@code name} and {@code value} exists, {@code false} otherwise. + *

+ * If {@code caseInsensitive} is {@code true} then a case insensitive compare is done on the value. + * + * @param name the name of the header to find + * @param value the value of the header to find + * @param caseInsensitive {@code true} then a case insensitive compare is run to compare values. + * otherwise a case sensitive compare is run to compare values. + * @return {@code true} if its contained, {@code false} otherwise. + */ + boolean contains(CharSequence name, CharSequence value, boolean caseInsensitive); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersFrame.java new file mode 100644 index 0000000..bf02e84 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersFrame.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * See HEADERS. + */ +public interface Http3HeadersFrame extends Http3RequestStreamFrame, Http3PushStreamFrame { + + @Override + default long type() { + return Http3CodecUtils.HTTP3_HEADERS_FRAME_TYPE; + } + + /** + * Returns the carried headers. + * + * @return the carried headers. + */ + Http3Headers headers(); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersSink.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersSink.java new file mode 100644 index 0000000..08cb97c --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersSink.java @@ -0,0 +1,165 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.handler.codec.http.HttpMethod; + +import java.util.function.BiConsumer; + +import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.getPseudoHeader; +import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.hasPseudoHeaderFormat; + +/** + * {@link BiConsumer} that does add header names and values to + * {@link Http3Headers} while also validate these. + */ +final class Http3HeadersSink implements BiConsumer { + private final Http3Headers headers; + private final long maxHeaderListSize; + private final boolean validate; + private final boolean trailer; + private long headersLength; + private boolean exceededMaxLength; + private Http3HeadersValidationException validationException; + private HeaderType previousType; + private boolean request; + private int pseudoHeadersCount; + + Http3HeadersSink(Http3Headers headers, long maxHeaderListSize, boolean validate, boolean trailer) { + this.headers = headers; + this.maxHeaderListSize = maxHeaderListSize; + this.validate = validate; + this.trailer = trailer; + } + + /** + * This method must be called after the sink is used. + */ + void finish() throws Http3HeadersValidationException, Http3Exception { + if (exceededMaxLength) { + throw new Http3Exception(Http3ErrorCode.H3_EXCESSIVE_LOAD, + String.format("Header size exceeded max allowed size (%d)", maxHeaderListSize)); + } + if (validationException != null) { + throw validationException; + } + if (validate) { + if (trailer) { + if (pseudoHeadersCount != 0) { + // Trailers must not have pseudo headers. + throw new Http3HeadersValidationException("Pseudo-header(s) included in trailers."); + } + return; + } + + // Validate that all mandatory pseudo-headers are included. + if (request) { + CharSequence method = headers.method(); + // fast-path + if (pseudoHeadersCount < 2) { + // There can't be any duplicates for pseudy header names. + throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included."); + } + if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) { + // For CONNECT we must only include: + // - :method + // - :authority + if (pseudoHeadersCount != 2 || headers.authority() == null) { + // There can't be any duplicates for pseudy header names. + throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included."); + } + } else { + // For requests we must include: + // - :method + // - :scheme + // - :authority + // - :path + if (pseudoHeadersCount != 4) { + // There can't be any duplicates for pseudy header names. + throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included."); + } + } + } else { + // For responses we must include: + // - :status + if (pseudoHeadersCount != 1) { + // There can't be any duplicates for pseudy header names. + throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included."); + } + } + } + } + + @Override + public void accept(CharSequence name, CharSequence value) { + headersLength += QpackHeaderField.sizeOf(name, value); + exceededMaxLength |= headersLength > maxHeaderListSize; + + if (exceededMaxLength || validationException != null) { + // We don't store the header since we've already failed validation requirements. + return; + } + + if (validate) { + try { + validate(headers, name); + } catch (Http3HeadersValidationException ex) { + validationException = ex; + return; + } + } + + headers.add(name, value); + } + + private void validate(Http3Headers headers, CharSequence name) { + if (hasPseudoHeaderFormat(name)) { + if (previousType == HeaderType.REGULAR_HEADER) { + throw new Http3HeadersValidationException( + String.format("Pseudo-header field '%s' found after regular header.", name)); + } + + final Http3Headers.PseudoHeaderName pseudoHeader = getPseudoHeader(name); + if (pseudoHeader == null) { + throw new Http3HeadersValidationException( + String.format("Invalid HTTP/3 pseudo-header '%s' encountered.", name)); + } + + final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ? + HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER; + if (previousType != null && currentHeaderType != previousType) { + throw new Http3HeadersValidationException("Mix of request and response pseudo-headers."); + } + + if (headers.contains(name)) { + // There can't be any duplicates for pseudy header names. + throw new Http3HeadersValidationException( + String.format("Pseudo-header field '%s' exists already.", name)); + } + pseudoHeadersCount++; + request = pseudoHeader.isRequestOnly(); + previousType = currentHeaderType; + } else { + previousType = HeaderType.REGULAR_HEADER; + } + } + + private enum HeaderType { + REGULAR_HEADER, + REQUEST_PSEUDO_HEADER, + RESPONSE_PSEUDO_HEADER + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersValidationException.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersValidationException.java new file mode 100644 index 0000000..c9c15ff --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3HeadersValidationException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * Thrown if {@link Http3Headers} validation fails for some reason. + */ +public final class Http3HeadersValidationException extends RuntimeException { + + /** + * Create a new instance. + * + * @param message the message. + */ + public Http3HeadersValidationException(String message) { + super(message); + } + + /** + * Create a new instance. + * + * @param message the message. + * @param cause the wrapped {@link Throwable}. + */ + public Http3HeadersValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3MaxPushIdFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3MaxPushIdFrame.java new file mode 100644 index 0000000..d184d9b --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3MaxPushIdFrame.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * See MAX_PUSH_ID. + */ +public interface Http3MaxPushIdFrame extends Http3ControlStreamFrame { + + @Override + default long type() { + return Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE; + } + + /** + * Returns the maximum value for a Push ID that the server can use. + * + * @return the id. + */ + long id(); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushPromiseFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushPromiseFrame.java new file mode 100644 index 0000000..b944dd8 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushPromiseFrame.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * See PUSH_PROMISE. + */ +public interface Http3PushPromiseFrame extends Http3RequestStreamFrame { + + @Override + default long type() { + return Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE; + } + + /** + * Returns the push id. + * + * @return the id. + */ + long id(); + + /** + * Returns the carried headers. + * + * @return the headers. + */ + Http3Headers headers(); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamClientInitializer.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamClientInitializer.java new file mode 100644 index 0000000..ca8c45b --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamClientInitializer.java @@ -0,0 +1,61 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.quic.QuicStreamChannel; + +import static io.netty.handler.codec.http3.Http3CodecUtils.isServerInitiatedQuicStream; +import static io.netty.handler.codec.http3.Http3RequestStreamCodecState.NO_STATE; + +/** + * Abstract base class that users can extend to init HTTP/3 push-streams for clients. This initializer + * will automatically add HTTP/3 codecs etc to the {@link ChannelPipeline} as well. + */ +public abstract class Http3PushStreamClientInitializer extends ChannelInitializer { + + @Override + protected final void initChannel(QuicStreamChannel ch) { + if (isServerInitiatedQuicStream(ch)) { + throw new IllegalArgumentException("Using client push stream initializer for server stream: " + + ch.streamId()); + } + Http3CodecUtils.verifyIsUnidirectional(ch); + + Http3ConnectionHandler connectionHandler = Http3CodecUtils.getConnectionHandlerOrClose(ch.parent()); + if (connectionHandler == null) { + // connection should have been closed + return; + } + ChannelPipeline pipeline = ch.pipeline(); + Http3RequestStreamDecodeStateValidator decodeStateValidator = new Http3RequestStreamDecodeStateValidator(); + // Add the encoder and decoder in the pipeline, so we can handle Http3Frames + pipeline.addLast(connectionHandler.newCodec(NO_STATE, decodeStateValidator)); + pipeline.addLast(decodeStateValidator); + // Add the handler that will validate what we write and receive on this stream. + pipeline.addLast(connectionHandler.newPushStreamValidationHandler(ch, decodeStateValidator)); + initPushStream(ch); + } + + /** + * Initialize the {@link QuicStreamChannel} to handle {@link Http3PushStreamFrame}s. At the point of calling this + * method it is already valid to write {@link Http3PushStreamFrame}s as the codec is already in the pipeline. + * + * @param ch the {QuicStreamChannel} for the push stream. + */ + protected abstract void initPushStream(QuicStreamChannel ch); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamClientValidationHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamClientValidationHandler.java new file mode 100644 index 0000000..fdc944a --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamClientValidationHandler.java @@ -0,0 +1,89 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; + +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.INVALID_FRAME_READ; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.sendStreamAbandonedIfRequired; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.validateDataFrameRead; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.validateHeaderFrameRead; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.validateOnStreamClosure; + +final class Http3PushStreamClientValidationHandler + extends Http3FrameTypeInboundValidationHandler { + private final QpackAttributes qpackAttributes; + private final QpackDecoder qpackDecoder; + private final Http3RequestStreamCodecState decodeState; + + private long expectedLength = -1; + private long seenLength; + + Http3PushStreamClientValidationHandler(QpackAttributes qpackAttributes, QpackDecoder qpackDecoder, + Http3RequestStreamCodecState decodeState) { + super(Http3RequestStreamFrame.class); + this.qpackAttributes = qpackAttributes; + this.qpackDecoder = qpackDecoder; + this.decodeState = decodeState; + } + + @Override + void channelRead(ChannelHandlerContext ctx, Http3RequestStreamFrame frame) { + if (frame instanceof Http3PushPromiseFrame) { + ctx.fireChannelRead(frame); + return; + } + + if (frame instanceof Http3HeadersFrame) { + Http3HeadersFrame headersFrame = (Http3HeadersFrame) frame; + long maybeContentLength = validateHeaderFrameRead(headersFrame, ctx, decodeState); + if (maybeContentLength >= 0) { + expectedLength = maybeContentLength; + } else if (maybeContentLength == INVALID_FRAME_READ) { + return; + } + } + + if (frame instanceof Http3DataFrame) { + final Http3DataFrame dataFrame = (Http3DataFrame) frame; + long maybeContentLength = validateDataFrameRead(dataFrame, ctx, expectedLength, seenLength, false); + if (maybeContentLength >= 0) { + seenLength = maybeContentLength; + } else if (maybeContentLength == INVALID_FRAME_READ) { + return; + } + } + ctx.fireChannelRead(frame); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + sendStreamAbandonedIfRequired(ctx, qpackAttributes, qpackDecoder, decodeState); + if (!validateOnStreamClosure(ctx, expectedLength, seenLength, false)) { + return; + } + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public boolean isSharable() { + // This handle keeps state so we can't share it. + return false; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamFrame.java new file mode 100644 index 0000000..a1485ce --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamFrame.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * Marker interface for frames that can be sent and received on a + * HTTP3 push stream. + */ +public interface Http3PushStreamFrame extends Http3Frame { +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamFrameTypeValidator.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamFrameTypeValidator.java new file mode 100644 index 0000000..3e48d6e --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamFrameTypeValidator.java @@ -0,0 +1,41 @@ +/* + * 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.handler.codec.http3; + +/** + * Validate that the frame type is valid for a push stream. + */ +final class Http3PushStreamFrameTypeValidator implements Http3FrameTypeValidator { + + static final Http3PushStreamFrameTypeValidator INSTANCE = new Http3PushStreamFrameTypeValidator(); + + private Http3PushStreamFrameTypeValidator() { } + + @Override + public void validate(long type, boolean first) throws Http3Exception { + switch ((int) type) { + case Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE: + case Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE: + case Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE: + case Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE: + case Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE: + throw new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Unexpected frame type '" + type + "' received"); + default: + break; + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamServerInitializer.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamServerInitializer.java new file mode 100644 index 0000000..7a6e3cb --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamServerInitializer.java @@ -0,0 +1,79 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.quic.QuicStreamChannel; + +import static io.netty.handler.codec.http3.Http3CodecUtils.isServerInitiatedQuicStream; +import static io.netty.handler.codec.http3.Http3CodecUtils.writeVariableLengthInteger; +import static io.netty.handler.codec.http3.Http3RequestStreamCodecState.NO_STATE; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * Abstract base class that users can extend to init HTTP/3 push-streams for servers. This initializer + * will automatically add HTTP/3 codecs etc to the {@link ChannelPipeline} as well. + */ +public abstract class Http3PushStreamServerInitializer extends ChannelInitializer { + + private final long pushId; + + protected Http3PushStreamServerInitializer(long pushId) { + this.pushId = checkPositiveOrZero(pushId, "pushId"); + } + + @Override + protected final void initChannel(QuicStreamChannel ch) { + if (!isServerInitiatedQuicStream(ch)) { + throw new IllegalArgumentException("Using server push stream initializer for client stream: " + + ch.streamId()); + } + Http3CodecUtils.verifyIsUnidirectional(ch); + + // We need to write stream type into the stream before doing anything else. + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1 + // Just allocate 16 bytes which would be the max needed to write 2 variable length ints. + ByteBuf buffer = ch.alloc().buffer(16); + writeVariableLengthInteger(buffer, Http3CodecUtils.HTTP3_PUSH_STREAM_TYPE); + writeVariableLengthInteger(buffer, pushId); + ch.write(buffer); + + Http3ConnectionHandler connectionHandler = Http3CodecUtils.getConnectionHandlerOrClose(ch.parent()); + if (connectionHandler == null) { + // connection should have been closed + return; + } + + ChannelPipeline pipeline = ch.pipeline(); + Http3RequestStreamEncodeStateValidator encodeStateValidator = new Http3RequestStreamEncodeStateValidator(); + // Add the encoder and decoder in the pipeline so we can handle Http3Frames + pipeline.addLast(connectionHandler.newCodec(encodeStateValidator, NO_STATE)); + pipeline.addLast(encodeStateValidator); + // Add the handler that will validate what we write and receive on this stream. + pipeline.addLast(connectionHandler.newPushStreamValidationHandler(ch, NO_STATE)); + initPushStream(ch); + } + + /** + * Initialize the {@link QuicStreamChannel} to handle {@link Http3PushStreamFrame}s. At the point of calling this + * method it is already valid to write {@link Http3PushStreamFrame}s as the codec is already in the pipeline. + * + * @param ch the {QuicStreamChannel} for the push stream. + */ + protected abstract void initPushStream(QuicStreamChannel ch); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamServerValidationHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamServerValidationHandler.java new file mode 100644 index 0000000..b8da4cd --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3PushStreamServerValidationHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * See HTTP Message Exchanges. + */ +final class Http3PushStreamServerValidationHandler + extends Http3FrameTypeOutboundValidationHandler { + + static final Http3PushStreamServerValidationHandler INSTANCE = new Http3PushStreamServerValidationHandler(); + + private Http3PushStreamServerValidationHandler() { + super(Http3PushStreamFrame.class); + } + + @Override + public boolean isSharable() { + return true; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamCodecState.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamCodecState.java new file mode 100644 index 0000000..f0663a1 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamCodecState.java @@ -0,0 +1,66 @@ +/* + * 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.handler.codec.http3; + +/** + * State of encoding or decoding for a stream following the + * HTTP message exchange semantics + */ +interface Http3RequestStreamCodecState { + /** + * An implementation of {@link Http3RequestStreamCodecState} that managed no state. + */ + Http3RequestStreamCodecState NO_STATE = new Http3RequestStreamCodecState() { + @Override + public boolean started() { + return false; + } + + @Override + public boolean receivedFinalHeaders() { + return false; + } + + @Override + public boolean terminated() { + return false; + } + }; + + /** + * If any {@link Http3HeadersFrame} or {@link Http3DataFrame} has been received/sent on this stream. + * + * @return {@code true} if any {@link Http3HeadersFrame} or {@link Http3DataFrame} has been received/sent on this + * stream. + */ + boolean started(); + + /** + * If a final {@link Http3HeadersFrame} has been received/sent before {@link Http3DataFrame} starts. + * + * @return {@code true} if a final {@link Http3HeadersFrame} has been received/sent before {@link Http3DataFrame} + * starts + */ + boolean receivedFinalHeaders(); + + /** + * If no more frames are expected on this stream. + * + * @return {@code true} if no more frames are expected on this stream. + */ + boolean terminated(); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamDecodeStateValidator.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamDecodeStateValidator.java new file mode 100644 index 0000000..c0db6c0 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamDecodeStateValidator.java @@ -0,0 +1,62 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http3.Http3RequestStreamEncodeStateValidator.State; + +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.frameTypeUnexpected; +import static io.netty.handler.codec.http3.Http3RequestStreamEncodeStateValidator.evaluateFrame; +import static io.netty.handler.codec.http3.Http3RequestStreamEncodeStateValidator.isFinalHeadersReceived; +import static io.netty.handler.codec.http3.Http3RequestStreamEncodeStateValidator.isStreamStarted; +import static io.netty.handler.codec.http3.Http3RequestStreamEncodeStateValidator.isTrailersReceived; + +final class Http3RequestStreamDecodeStateValidator extends ChannelInboundHandlerAdapter + implements Http3RequestStreamCodecState { + private State state = State.None; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof Http3RequestStreamFrame)) { + super.channelRead(ctx, msg); + return; + } + final Http3RequestStreamFrame frame = (Http3RequestStreamFrame) msg; + final State nextState = evaluateFrame(state, frame); + if (nextState == null) { + frameTypeUnexpected(ctx, msg); + return; + } + state = nextState; + super.channelRead(ctx, msg); + } + + @Override + public boolean started() { + return isStreamStarted(state); + } + + @Override + public boolean receivedFinalHeaders() { + return isFinalHeadersReceived(state); + } + + @Override + public boolean terminated() { + return isTrailersReceived(state); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamEncodeStateValidator.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamEncodeStateValidator.java new file mode 100644 index 0000000..6e2460f --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamEncodeStateValidator.java @@ -0,0 +1,122 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpStatusClass; + +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.frameTypeUnexpected; + +final class Http3RequestStreamEncodeStateValidator extends ChannelOutboundHandlerAdapter + implements Http3RequestStreamCodecState { + enum State { + None, + Headers, + FinalHeaders, + Trailers + } + private State state = State.None; + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof Http3RequestStreamFrame)) { + super.write(ctx, msg, promise); + return; + } + final Http3RequestStreamFrame frame = (Http3RequestStreamFrame) msg; + final State nextState = evaluateFrame(state, frame); + if (nextState == null) { + frameTypeUnexpected(ctx, msg); + return; + } + state = nextState; + super.write(ctx, msg, promise); + } + + @Override + public boolean started() { + return isStreamStarted(state); + } + + @Override + public boolean receivedFinalHeaders() { + return isFinalHeadersReceived(state); + } + + @Override + public boolean terminated() { + return isTrailersReceived(state); + } + + /** + * Evaluates the passed frame and returns the following: + *

    + *
  • Modified {@link State} if the state should be changed.
  • + *
  • Same {@link State} as the passed {@code state} if no state change is necessary
  • + *
  • {@code null} if the frame is unexpected
  • + *
+ * + * @param state Current state. + * @param frame to evaluate. + * @return Next {@link State} or {@code null} if the frame is invalid. + */ + static State evaluateFrame(State state, Http3RequestStreamFrame frame) { + if (frame instanceof Http3PushPromiseFrame || frame instanceof Http3UnknownFrame) { + // always allow push promise frames. + return state; + } + switch (state) { + case None: + case Headers: + if (!(frame instanceof Http3HeadersFrame)) { + return null; + } + return isInformationalResponse((Http3HeadersFrame) frame) ? State.Headers : State.FinalHeaders; + case FinalHeaders: + if (frame instanceof Http3HeadersFrame) { + if (isInformationalResponse((Http3HeadersFrame) frame)) { + // Information response after final response headers + return null; + } + // trailers + return State.Trailers; + } + return state; + case Trailers: + return null; + default: + throw new Error(); + } + } + + static boolean isStreamStarted(State state) { + return state != State.None; + } + + static boolean isFinalHeadersReceived(State state) { + return isStreamStarted(state) && state != State.Headers; + } + + static boolean isTrailersReceived(State state) { + return state == State.Trailers; + } + + private static boolean isInformationalResponse(Http3HeadersFrame headersFrame) { + return HttpStatusClass.valueOf(headersFrame.headers().status()) == HttpStatusClass.INFORMATIONAL; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamFrame.java new file mode 100644 index 0000000..b4c7ffb --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamFrame.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +/** + * Marker interface for frames that can be sent and received on a + * HTTP3 request stream. + */ +public interface Http3RequestStreamFrame extends Http3Frame { +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamFrameTypeValidator.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamFrameTypeValidator.java new file mode 100644 index 0000000..26f7044 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamFrameTypeValidator.java @@ -0,0 +1,40 @@ +/* + * 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.handler.codec.http3; + +/** + * Validate that the frame type is valid for a request stream. + */ +final class Http3RequestStreamFrameTypeValidator implements Http3FrameTypeValidator { + + static final Http3RequestStreamFrameTypeValidator INSTANCE = new Http3RequestStreamFrameTypeValidator(); + + private Http3RequestStreamFrameTypeValidator() { } + + @Override + public void validate(long type, boolean first) throws Http3Exception { + switch ((int) type) { + case Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE: + case Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE: + case Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE: + case Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE: + throw new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Unexpected frame type '" + type + "' received"); + default: + break; + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamInboundHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamInboundHandler.java new file mode 100644 index 0000000..09492e3 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamInboundHandler.java @@ -0,0 +1,135 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.handler.codec.quic.QuicException; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * {@link ChannelInboundHandlerAdapter} which makes it easy to handle + * HTTP3 request streams. + */ +public abstract class Http3RequestStreamInboundHandler extends ChannelInboundHandlerAdapter { + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(Http3RequestStreamInboundHandler.class); + + @Override + public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http3UnknownFrame) { + channelRead(ctx, (Http3UnknownFrame) msg); + } else if (msg instanceof Http3HeadersFrame) { + channelRead(ctx, (Http3HeadersFrame) msg); + } else if (msg instanceof Http3DataFrame) { + channelRead(ctx, (Http3DataFrame) msg); + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt == ChannelInputShutdownEvent.INSTANCE) { + channelInputClosed(ctx); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof QuicException) { + handleQuicException(ctx, (QuicException) cause); + } else if (cause instanceof Http3Exception) { + handleHttp3Exception(ctx, (Http3Exception) cause); + } else { + ctx.fireExceptionCaught(cause); + } + } + + /** + * Called once a {@link Http3HeadersFrame} is ready for this stream to process. + * + * @param ctx the {@link ChannelHandlerContext} of this handler. + * @param frame the {@link Http3HeadersFrame} that was read + * @throws Exception thrown if an error happens during processing. + */ + protected abstract void channelRead(ChannelHandlerContext ctx, Http3HeadersFrame frame) throws Exception; + + /** + * Called once a {@link Http3DataFrame} is ready for this stream to process. + * + * @param ctx the {@link ChannelHandlerContext} of this handler. + * @param frame the {@link Http3DataFrame} that was read + * @throws Exception thrown if an error happens during processing. + */ + protected abstract void channelRead(ChannelHandlerContext ctx, Http3DataFrame frame) throws Exception; + + /** + * Called once the input is closed and so no more inbound data is received on it. + * + * @param ctx the {@link ChannelHandlerContext} of this handler. + * @throws Exception thrown if an error happens during processing. + */ + protected abstract void channelInputClosed(ChannelHandlerContext ctx) throws Exception; + + /** + * Called once a {@link Http3UnknownFrame} is ready for this stream to process. By default these frames are just + * released and so dropped on the floor as stated in the RFC. That said you may want to override this method if + * you use some custom frames which are not part of the main spec. + * + * @param ctx the {@link ChannelHandlerContext} of this handler. + * @param frame the {@link Http3UnknownFrame} that was read + */ + protected void channelRead(@SuppressWarnings("unused") ChannelHandlerContext ctx, Http3UnknownFrame frame) { + frame.release(); + } + + /** + * Called once a {@link QuicException} should be handled. + * + * @param ctx the {@link ChannelHandlerContext} of this handler. + * @param exception the {@link QuicException} that caused the error. + */ + protected void handleQuicException(@SuppressWarnings("unused") ChannelHandlerContext ctx, QuicException exception) { + logger.debug("Caught QuicException on channel {}", ctx.channel(), exception); + } + + /** + * Called once a {@link Http3Exception} should be handled. + * + * @param ctx the {@link ChannelHandlerContext} of this handler. + * @param exception the {@link Http3Exception} that caused the error. + */ + protected void handleHttp3Exception(@SuppressWarnings("unused") ChannelHandlerContext ctx, + Http3Exception exception) { + logger.error("Caught Http3Exception on channel {}", ctx.channel(), exception); + } + + /** + * Return the local control stream for this HTTP/3 connection. This can be used to send + * {@link Http3ControlStreamFrame}s to the remote peer. + * + * @param ctx the {@link ChannelHandlerContext} of this handler. + * @return the control stream. + */ + protected final QuicStreamChannel controlStream(ChannelHandlerContext ctx) { + return Http3.getLocalControlStream(ctx.channel().parent()); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamInitializer.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamInitializer.java new file mode 100644 index 0000000..0afba0c --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamInitializer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.internal.StringUtil; + +/** + * Abstract base class that users can extend to init HTTP/3 request-streams. This initializer + * will automatically add HTTP/3 codecs etc to the {@link ChannelPipeline} as well. + */ +public abstract class Http3RequestStreamInitializer extends ChannelInitializer { + + @Override + protected final void initChannel(QuicStreamChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + Http3ConnectionHandler connectionHandler = ch.parent().pipeline().get(Http3ConnectionHandler.class); + if (connectionHandler == null) { + throw new IllegalStateException("Couldn't obtain the " + + StringUtil.simpleClassName(Http3ConnectionHandler.class) + " of the parent Channel"); + } + + Http3RequestStreamEncodeStateValidator encodeStateValidator = new Http3RequestStreamEncodeStateValidator(); + Http3RequestStreamDecodeStateValidator decodeStateValidator = new Http3RequestStreamDecodeStateValidator(); + + // Add the encoder and decoder in the pipeline so we can handle Http3Frames + pipeline.addLast(connectionHandler.newCodec(encodeStateValidator, decodeStateValidator)); + // Add the handler that will validate what we write and receive on this stream. + pipeline.addLast(encodeStateValidator); + pipeline.addLast(decodeStateValidator); + pipeline.addLast(connectionHandler.newRequestStreamValidationHandler(ch, encodeStateValidator, + decodeStateValidator)); + initRequestStream(ch); + } + + /** + * Init the {@link QuicStreamChannel} to handle {@link Http3RequestStreamFrame}s. At the point of calling this + * method it is already valid to write {@link Http3RequestStreamFrame}s as the codec is already in the pipeline. + * @param ch the {QuicStreamChannel} for the request stream. + */ + protected abstract void initRequestStream(QuicStreamChannel ch); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamValidationHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamValidationHandler.java new file mode 100644 index 0000000..2ef23ce --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamValidationHandler.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; + +import java.util.function.BooleanSupplier; + +import static io.netty.handler.codec.http.HttpMethod.HEAD; +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.frameTypeUnexpected; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.INVALID_FRAME_READ; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.sendStreamAbandonedIfRequired; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.validateClientWrite; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.validateDataFrameRead; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.validateHeaderFrameRead; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationUtils.validateOnStreamClosure; + +final class Http3RequestStreamValidationHandler extends Http3FrameTypeDuplexValidationHandler { + private final boolean server; + private final BooleanSupplier goAwayReceivedSupplier; + private final QpackAttributes qpackAttributes; + private final QpackDecoder qpackDecoder; + private final Http3RequestStreamCodecState decodeState; + private final Http3RequestStreamCodecState encodeState; + + private boolean clientHeadRequest; + private long expectedLength = -1; + private long seenLength; + + static ChannelHandler newServerValidator(QpackAttributes qpackAttributes, QpackDecoder decoder, + Http3RequestStreamCodecState encodeState, + Http3RequestStreamCodecState decodeState) { + return new Http3RequestStreamValidationHandler(true, () -> false, qpackAttributes, decoder, + encodeState, decodeState); + } + + static ChannelHandler newClientValidator(BooleanSupplier goAwayReceivedSupplier, QpackAttributes qpackAttributes, + QpackDecoder decoder, Http3RequestStreamCodecState encodeState, + Http3RequestStreamCodecState decodeState) { + return new Http3RequestStreamValidationHandler(false, goAwayReceivedSupplier, qpackAttributes, decoder, + encodeState, decodeState); + } + + private Http3RequestStreamValidationHandler(boolean server, BooleanSupplier goAwayReceivedSupplier, + QpackAttributes qpackAttributes, QpackDecoder qpackDecoder, + Http3RequestStreamCodecState encodeState, + Http3RequestStreamCodecState decodeState) { + super(Http3RequestStreamFrame.class); + this.server = server; + this.goAwayReceivedSupplier = goAwayReceivedSupplier; + this.qpackAttributes = qpackAttributes; + this.qpackDecoder = qpackDecoder; + this.decodeState = decodeState; + this.encodeState = encodeState; + } + + @Override + void write(ChannelHandlerContext ctx, Http3RequestStreamFrame frame, ChannelPromise promise) { + if (!server) { + if (!validateClientWrite(frame, promise, ctx, goAwayReceivedSupplier, encodeState)) { + return; + } + if (frame instanceof Http3HeadersFrame) { + clientHeadRequest = HEAD.asciiName().equals(((Http3HeadersFrame) frame).headers().method()); + } + } + ctx.write(frame, promise); + } + + @Override + void channelRead(ChannelHandlerContext ctx, Http3RequestStreamFrame frame) { + if (frame instanceof Http3PushPromiseFrame) { + if (server) { + // Server should not receive a push promise + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-push_promise + frameTypeUnexpected(ctx, frame); + } else { + ctx.fireChannelRead(frame); + } + return; + } + + if (frame instanceof Http3HeadersFrame) { + Http3HeadersFrame headersFrame = (Http3HeadersFrame) frame; + long maybeContentLength = validateHeaderFrameRead(headersFrame, ctx, decodeState); + if (maybeContentLength >= 0) { + expectedLength = maybeContentLength; + } else if (maybeContentLength == INVALID_FRAME_READ) { + return; + } + } + + if (frame instanceof Http3DataFrame) { + final Http3DataFrame dataFrame = (Http3DataFrame) frame; + long maybeContentLength = validateDataFrameRead(dataFrame, ctx, expectedLength, seenLength, + clientHeadRequest); + if (maybeContentLength >= 0) { + seenLength = maybeContentLength; + } else if (maybeContentLength == INVALID_FRAME_READ) { + return; + } + } + + ctx.fireChannelRead(frame); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + sendStreamAbandonedIfRequired(ctx, qpackAttributes, qpackDecoder, decodeState); + if (!validateOnStreamClosure(ctx, expectedLength, seenLength, clientHeadRequest)) { + return; + } + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public boolean isSharable() { + // This handle keeps state so we can't share it. + return false; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamValidationUtils.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamValidationUtils.java new file mode 100644 index 0000000..9c580bf --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3RequestStreamValidationUtils.java @@ -0,0 +1,158 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.StringUtil; + +import java.util.function.BooleanSupplier; + +import static io.netty.handler.codec.http.HttpUtil.normalizeAndGetContentLength; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_MESSAGE_ERROR; +import static io.netty.handler.codec.http3.Http3FrameValidationUtils.frameTypeUnexpected; + +final class Http3RequestStreamValidationUtils { + static final long CONTENT_LENGTH_NOT_MODIFIED = -1; + static final long INVALID_FRAME_READ = -2; + + private Http3RequestStreamValidationUtils() { + // No instances + } + + /** + * Validate write of the passed {@link Http3RequestStreamFrame} for a client and takes appropriate error handling + * for invalid frames. + * + * @param frame to validate. + * @param promise for the write. + * @param ctx for the handler. + * @param goAwayReceivedSupplier for the channel. + * @param encodeState for the stream. + * @return {@code true} if the frame is valid. + */ + static boolean validateClientWrite(Http3RequestStreamFrame frame, ChannelPromise promise, ChannelHandlerContext ctx, + BooleanSupplier goAwayReceivedSupplier, + Http3RequestStreamCodecState encodeState) { + if (goAwayReceivedSupplier.getAsBoolean() && !encodeState.started()) { + String type = StringUtil.simpleClassName(frame); + ReferenceCountUtil.release(frame); + promise.setFailure(new Http3Exception(Http3ErrorCode.H3_FRAME_UNEXPECTED, + "Frame of type " + type + " unexpected as we received a GOAWAY already.")); + ctx.close(); + return false; + } + if (frame instanceof Http3PushPromiseFrame) { + // Only supported on the server. + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-4.1 + frameTypeUnexpected(promise, frame); + return false; + } + return true; + } + + static long validateHeaderFrameRead(Http3HeadersFrame headersFrame, ChannelHandlerContext ctx, + Http3RequestStreamCodecState decodeState) { + if (headersFrame.headers().contains(HttpHeaderNames.CONNECTION)) { + headerUnexpected(ctx, headersFrame, "connection header included"); + return INVALID_FRAME_READ; + } + CharSequence value = headersFrame.headers().get(HttpHeaderNames.TE); + if (value != null && !HttpHeaderValues.TRAILERS.equals(value)) { + headerUnexpected(ctx, headersFrame, "te header field included with invalid value: " + value); + return INVALID_FRAME_READ; + } + if (decodeState.receivedFinalHeaders()) { + long length = normalizeAndGetContentLength( + headersFrame.headers().getAll(HttpHeaderNames.CONTENT_LENGTH), false, true); + if (length != CONTENT_LENGTH_NOT_MODIFIED) { + headersFrame.headers().setLong(HttpHeaderNames.CONTENT_LENGTH, length); + } + return length; + } + return CONTENT_LENGTH_NOT_MODIFIED; + } + + static long validateDataFrameRead(Http3DataFrame dataFrame, ChannelHandlerContext ctx, + long expectedLength, long seenLength, boolean clientHeadRequest) { + try { + return verifyContentLength(dataFrame.content().readableBytes(), expectedLength, seenLength, false, + clientHeadRequest); + } catch (Http3Exception e) { + ReferenceCountUtil.release(dataFrame); + failStream(ctx, e); + return INVALID_FRAME_READ; + } + } + + static boolean validateOnStreamClosure(ChannelHandlerContext ctx, long expectedLength, long seenLength, + boolean clientHeadRequest) { + try { + verifyContentLength(0, expectedLength, seenLength, true, clientHeadRequest); + return true; + } catch (Http3Exception e) { + ctx.fireExceptionCaught(e); + Http3CodecUtils.streamError(ctx, e.errorCode()); + return false; + } + } + + static void sendStreamAbandonedIfRequired(ChannelHandlerContext ctx, QpackAttributes qpackAttributes, + QpackDecoder qpackDecoder, Http3RequestStreamCodecState decodeState) { + if (!qpackAttributes.dynamicTableDisabled() && !decodeState.terminated()) { + final long streamId = ((QuicStreamChannel) ctx.channel()).streamId(); + if (qpackAttributes.decoderStreamAvailable()) { + qpackDecoder.streamAbandoned(qpackAttributes.decoderStream(), streamId); + } else { + qpackAttributes.whenDecoderStreamAvailable(future -> { + if (future.isSuccess()) { + qpackDecoder.streamAbandoned(qpackAttributes.decoderStream(), streamId); + } + }); + } + } + } + + private static void headerUnexpected(ChannelHandlerContext ctx, Http3RequestStreamFrame frame, String msg) { + // We should close the stream. + // See https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1 + ReferenceCountUtil.release(frame); + failStream(ctx, new Http3Exception(H3_MESSAGE_ERROR, msg)); + } + + private static void failStream(ChannelHandlerContext ctx, Http3Exception cause) { + ctx.fireExceptionCaught(cause); + Http3CodecUtils.streamError(ctx, cause.errorCode()); + } + + // See https://tools.ietf.org/html/draft-ietf-quic-http-34#section-4.1.3 + private static long verifyContentLength(int length, long expectedLength, long seenLength, boolean end, + boolean clientHeadRequest) throws Http3Exception { + seenLength += length; + if (expectedLength != -1 && (seenLength > expectedLength || + (!clientHeadRequest && end && seenLength != expectedLength))) { + throw new Http3Exception( + H3_MESSAGE_ERROR, "Expected content-length " + expectedLength + + " != " + seenLength + "."); + } + return seenLength; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ServerConnectionHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ServerConnectionHandler.java new file mode 100644 index 0000000..238357f --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ServerConnectionHandler.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.internal.ObjectUtil; + +import java.util.function.LongFunction; + +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY; + + +/** + * Handler that handles HTTP3 for the server-side. + */ +public final class Http3ServerConnectionHandler extends Http3ConnectionHandler { + private final ChannelHandler requestStreamHandler; + + /** + * Create a new instance. + * + * @param requestStreamHandler the {@link ChannelHandler} that is used for each new request stream. + * This handler will receive {@link Http3HeadersFrame} and {@link Http3DataFrame}s. + */ + public Http3ServerConnectionHandler(ChannelHandler requestStreamHandler) { + this(requestStreamHandler, null, null, null, true); + } + + /** + * Create a new instance. + * @param requestStreamHandler the {@link ChannelHandler} that is used for each new request stream. + * This handler will receive {@link Http3HeadersFrame} and + * {@link Http3DataFrame}s. + * @param inboundControlStreamHandler the {@link ChannelHandler} which will be notified about + * {@link Http3RequestStreamFrame}s or {@code null} if the user is not + * interested in these. + * @param unknownInboundStreamHandlerFactory the {@link LongFunction} that will provide a custom + * {@link ChannelHandler} for unknown inbound stream types or + * {@code null} if no special handling should be done. + * @param localSettings the local {@link Http3SettingsFrame} that should be sent to the + * remote peer or {@code null} if the default settings should be used. + * @param disableQpackDynamicTable If QPACK dynamic table should be disabled. + */ + public Http3ServerConnectionHandler(ChannelHandler requestStreamHandler, + ChannelHandler inboundControlStreamHandler, + LongFunction unknownInboundStreamHandlerFactory, + Http3SettingsFrame localSettings, boolean disableQpackDynamicTable) { + super(true, inboundControlStreamHandler, unknownInboundStreamHandlerFactory, localSettings, + disableQpackDynamicTable); + this.requestStreamHandler = ObjectUtil.checkNotNull(requestStreamHandler, "requestStreamHandler"); + } + + @Override + void initBidirectionalStream(ChannelHandlerContext ctx, QuicStreamChannel streamChannel) { + ChannelPipeline pipeline = streamChannel.pipeline(); + Http3RequestStreamEncodeStateValidator encodeStateValidator = new Http3RequestStreamEncodeStateValidator(); + Http3RequestStreamDecodeStateValidator decodeStateValidator = new Http3RequestStreamDecodeStateValidator(); + // Add the encoder and decoder in the pipeline so we can handle Http3Frames + pipeline.addLast(newCodec(encodeStateValidator, decodeStateValidator)); + pipeline.addLast(encodeStateValidator); + pipeline.addLast(decodeStateValidator); + pipeline.addLast(newRequestStreamValidationHandler(streamChannel, encodeStateValidator, decodeStateValidator)); + pipeline.addLast(requestStreamHandler); + } + + @Override + void initUnidirectionalStream(ChannelHandlerContext ctx, QuicStreamChannel streamChannel) { + final Long maxTableCapacity = remoteControlStreamHandler.localSettings() + .get(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY); + streamChannel.pipeline().addLast( + new Http3UnidirectionalStreamInboundServerHandler(codecFactory, + localControlStreamHandler, remoteControlStreamHandler, + unknownInboundStreamHandlerFactory, + () -> new QpackEncoderHandler(maxTableCapacity, qpackDecoder), + () -> new QpackDecoderHandler(qpackEncoder))); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ServerPushStreamManager.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ServerPushStreamManager.java new file mode 100644 index 0000000..d4491d3 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3ServerPushStreamManager.java @@ -0,0 +1,289 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamChannelBootstrap; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.function.UnaryOperator; + +import static io.netty.handler.codec.http3.Http3.maxPushIdReceived; +import static io.netty.handler.codec.http3.Http3CodecUtils.connectionError; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_ID_ERROR; +import static io.netty.util.internal.PlatformDependent.newConcurrentHashMap; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.atomic.AtomicLongFieldUpdater.newUpdater; + +/** + * A manager for push streams + * for a server. New push streams can be initiated using the various {@code newPushStream} methods. It is required to + * add the {@link ChannelHandler} returned from {@link #controlStreamListener()} to the {@link QuicChannel} associated + * with this manager. + */ +public final class Http3ServerPushStreamManager { + private static final AtomicLongFieldUpdater nextIdUpdater = + newUpdater(Http3ServerPushStreamManager.class, "nextId"); + private static final Object CANCELLED_STREAM = new Object(); + private static final Object PUSH_ID_GENERATED = new Object(); + private static final Object AWAITING_STREAM_ESTABLISHMENT = new Object(); + + private final QuicChannel channel; + private final ConcurrentMap pushStreams; + private final ChannelInboundHandler controlStreamListener; + + private volatile long nextId; + + /** + * Creates a new instance. + * + * @param channel for which this manager is created. + */ + public Http3ServerPushStreamManager(QuicChannel channel) { + this(channel, 8); + } + + /** + * Creates a new instance. + * + * @param channel for which this manager is created. + * @param initialPushStreamsCountHint a hint for the number of push streams that may be created. + */ + public Http3ServerPushStreamManager(QuicChannel channel, int initialPushStreamsCountHint) { + this.channel = requireNonNull(channel, "channel"); + pushStreams = newConcurrentHashMap(initialPushStreamsCountHint); + controlStreamListener = new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof Http3CancelPushFrame) { + final long pushId = ((Http3CancelPushFrame) msg).id(); + if (pushId >= nextId) { + connectionError(ctx, H3_ID_ERROR, "CANCEL_PUSH id greater than the last known id", true); + return; + } + + pushStreams.computeIfPresent(pushId, (id, existing) -> { + if (existing == AWAITING_STREAM_ESTABLISHMENT) { + return CANCELLED_STREAM; + } + if (existing == PUSH_ID_GENERATED) { + throw new IllegalStateException("Unexpected push stream state " + existing + + " for pushId: " + id); + } + assert existing instanceof QuicStreamChannel; + ((QuicStreamChannel) existing).close(); + // remove the push stream from the map. + return null; + }); + } + ReferenceCountUtil.release(msg); + } + }; + } + + /** + * Returns {@code true} if server push is allowed at this point. + * + * @return {@code true} if server push is allowed at this point. + */ + public boolean isPushAllowed() { + return isPushAllowed(maxPushIdReceived(channel)); + } + + /** + * Reserves a push ID to be used to create a new push stream subsequently. A push ID can only be used to create + * exactly one push stream. + * + * @return Next push ID. + * @throws IllegalStateException If it is not allowed to create any more push streams on the associated + * {@link QuicChannel}. Use {@link #isPushAllowed()} to check if server push is allowed. + */ + public long reserveNextPushId() { + final long maxPushId = maxPushIdReceived(channel); + if (isPushAllowed(maxPushId)) { + return nextPushId(); + } + throw new IllegalStateException("MAX allowed push ID: " + maxPushId + ", next push ID: " + nextId); + } + + /** + * Returns a new HTTP/3 push-stream that will use the given {@link ChannelHandler} + * to dispatch {@link Http3PushStreamFrame}s too. The needed HTTP/3 codecs are automatically added to the + * pipeline as well. + * + * @param pushId for the push stream. This MUST be obtained using {@link #reserveNextPushId()}. + * @param handler the {@link ChannelHandler} to add. Can be {@code null}. + * @return the {@link Future} that will be notified once the push-stream was opened. + */ + public Future newPushStream(long pushId, ChannelHandler handler) { + final Promise promise = channel.eventLoop().newPromise(); + newPushStream(pushId, handler, promise); + return promise; + } + + /** + * Returns a new HTTP/3 push-stream that will use the given {@link ChannelHandler} + * to dispatch {@link Http3PushStreamFrame}s too. The needed HTTP/3 codecs are automatically added to the + * pipeline as well. + * + * @param pushId for the push stream. This MUST be obtained using {@link #reserveNextPushId()}. + * @param handler the {@link ChannelHandler} to add. Can be {@code null}. + * @param promise to indicate creation of the push stream. + */ + public void newPushStream(long pushId, ChannelHandler handler, Promise promise) { + validatePushId(pushId); + channel.createStream(QuicStreamType.UNIDIRECTIONAL, pushStreamInitializer(pushId, handler), promise); + setupCancelPushIfStreamCreationFails(pushId, promise, channel); + } + + /** + * Returns a new HTTP/3 push-stream that will use the given {@link ChannelHandler} + * to dispatch {@link Http3PushStreamFrame}s too. The needed HTTP/3 codecs are automatically added to the + * pipeline as well. + * + * @param pushId for the push stream. This MUST be obtained using {@link #reserveNextPushId()}. + * @param handler the {@link ChannelHandler} to add. Can be {@code null}. + * @param bootstrapConfigurator {@link UnaryOperator} to configure the {@link QuicStreamChannelBootstrap} used. + * @param promise to indicate creation of the push stream. + */ + public void newPushStream(long pushId, ChannelHandler handler, + UnaryOperator bootstrapConfigurator, + Promise promise) { + validatePushId(pushId); + QuicStreamChannelBootstrap bootstrap = bootstrapConfigurator.apply(channel.newStreamBootstrap()); + bootstrap.type(QuicStreamType.UNIDIRECTIONAL) + .handler(pushStreamInitializer(pushId, handler)) + .create(promise); + setupCancelPushIfStreamCreationFails(pushId, promise, channel); + } + + /** + * A {@link ChannelInboundHandler} to be added to the {@link QuicChannel} associated with this + * {@link Http3ServerPushStreamManager} to listen to control stream frames. + * + * @return {@link ChannelInboundHandler} to be added to the {@link QuicChannel} associated with this + * {@link Http3ServerPushStreamManager} to listen to control stream frames. + */ + public ChannelInboundHandler controlStreamListener() { + return controlStreamListener; + } + + private boolean isPushAllowed(long maxPushId) { + return nextId <= maxPushId; + } + + private long nextPushId() { + final long pushId = nextIdUpdater.getAndIncrement(this); + pushStreams.put(pushId, PUSH_ID_GENERATED); + return pushId; + } + + private void validatePushId(long pushId) { + if (!pushStreams.replace(pushId, PUSH_ID_GENERATED, AWAITING_STREAM_ESTABLISHMENT)) { + throw new IllegalArgumentException("Unknown push ID: " + pushId); + } + } + + private Http3PushStreamServerInitializer pushStreamInitializer(long pushId, ChannelHandler handler) { + final Http3PushStreamServerInitializer initializer; + if (handler instanceof Http3PushStreamServerInitializer) { + initializer = (Http3PushStreamServerInitializer) handler; + } else { + initializer = null; + } + return new Http3PushStreamServerInitializer(pushId) { + @Override + protected void initPushStream(QuicStreamChannel ch) { + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + private boolean stateUpdated; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (!stateUpdated) { + updatePushStreamsMap(); + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + if (!stateUpdated && ctx.channel().isActive()) { + updatePushStreamsMap(); + } + } + + private void updatePushStreamsMap() { + assert !stateUpdated; + stateUpdated = true; + pushStreams.compute(pushId, (id, existing) -> { + if (existing == AWAITING_STREAM_ESTABLISHMENT) { + return ch; + } + if (existing == CANCELLED_STREAM) { + ch.close(); + return null; // remove push stream. + } + throw new IllegalStateException("Unexpected push stream state " + + existing + " for pushId: " + id); + }); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + pushStreams.remove(pushId); + } + ctx.fireUserEventTriggered(evt); + } + }); + if (initializer != null) { + initializer.initPushStream(ch); + } else if (handler != null) { + ch.pipeline().addLast(handler); + } + } + }; + } + + private static void setupCancelPushIfStreamCreationFails(long pushId, Future future, + QuicChannel channel) { + if (future.isDone()) { + sendCancelPushIfFailed(future, pushId, channel); + } else { + future.addListener(f -> sendCancelPushIfFailed(future, pushId, channel)); + } + } + + private static void sendCancelPushIfFailed(Future future, long pushId, QuicChannel channel) { + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push + // If we can not establish the stream, we can not send the promised push response, so send a CANCEL_PUSH + if (!future.isSuccess()) { + final QuicStreamChannel localControlStream = Http3.getLocalControlStream(channel); + assert localControlStream != null; + localControlStream.writeAndFlush(new DefaultHttp3CancelPushFrame(pushId)); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3SettingsFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3SettingsFrame.java new file mode 100644 index 0000000..ed0de15 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3SettingsFrame.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import java.util.Map; + +/** + * See SETTINGS. + */ +public interface Http3SettingsFrame extends Http3ControlStreamFrame, Iterable> { + + /** + * See + * SETTINGS_QPACK_MAX_TABLE_CAPACITY. + */ + long HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0x1; + /** + * See + * SETTINGS_QPACK_BLOCKED_STREAMS. + */ + long HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS = 0x7; + /** + * See + * SETTINGS_MAX_FIELD_SECTION_SIZE. + */ + long HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE = 0x6; + + @Override + default long type() { + return Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE; + } + + /** + * Get a setting from the frame. + * + * @param key the key of the setting. + * @return the value of the setting or {@code null} if none was found with the given key. + */ + Long get(long key); + + /** + * Get a setting from the frame. + * + * @param key the key of the setting. + * @param defaultValue If the setting does not exist. + * @return the value of the setting or {@code defaultValue} if none was found with the given key. + */ + default Long getOrDefault(long key, long defaultValue) { + final Long val = get(key); + return val == null ? defaultValue : val; + } + + /** + * Put a setting in the frame. + * + * @param key the key of the setting + * @param value the value of the setting. + * @return the previous stored valued for the given key or {@code null} if none was stored before. + */ + Long put(long key, Long value); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundClientHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundClientHandler.java new file mode 100644 index 0000000..c75a7f5 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundClientHandler.java @@ -0,0 +1,59 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http3.Http3FrameCodec.Http3FrameCodecFactory; + +import java.util.function.LongFunction; +import java.util.function.Supplier; + +final class Http3UnidirectionalStreamInboundClientHandler extends Http3UnidirectionalStreamInboundHandler { + private final LongFunction pushStreamHandlerFactory; + + Http3UnidirectionalStreamInboundClientHandler( + Http3FrameCodecFactory codecFactory, + Http3ControlStreamInboundHandler localControlStreamHandler, + Http3ControlStreamOutboundHandler remoteControlStreamHandler, + LongFunction unknownStreamHandlerFactory, + LongFunction pushStreamHandlerFactory, + Supplier qpackEncoderHandlerFactory, Supplier qpackDecoderHandlerFactory) { + super(codecFactory, localControlStreamHandler, remoteControlStreamHandler, unknownStreamHandlerFactory, + qpackEncoderHandlerFactory, qpackDecoderHandlerFactory); + this.pushStreamHandlerFactory = pushStreamHandlerFactory == null ? __ -> ReleaseHandler.INSTANCE : + pushStreamHandlerFactory; + } + + @Override + void initPushStream(ChannelHandlerContext ctx, long pushId) { + // See https://tools.ietf.org/html/draft-ietf-quic-http-32#section-4.4 + Long maxPushId = remoteControlStreamHandler.sentMaxPushId(); + if (maxPushId == null) { + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_ID_ERROR, + "Received push stream before sending MAX_PUSH_ID frame.", false); + } else if (maxPushId < pushId) { + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_ID_ERROR, + "Received push stream with ID " + pushId + " greater than the max push ID " + maxPushId + + '.', false); + } else { + // Replace this handler with the actual push stream handlers. + final ChannelHandler pushStreamHandler = pushStreamHandlerFactory.apply(pushId); + ctx.pipeline().replace(this, null, pushStreamHandler); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundHandler.java new file mode 100644 index 0000000..f0cdccb --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundHandler.java @@ -0,0 +1,191 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +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.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.http3.Http3FrameCodec.Http3FrameCodecFactory; +import io.netty.util.AttributeKey; +import io.netty.util.ReferenceCountUtil; + +import java.util.List; +import java.util.function.LongFunction; +import java.util.function.Supplier; + +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_CONTROL_STREAM_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_PUSH_STREAM_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_QPACK_DECODER_STREAM_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_QPACK_ENCODER_STREAM_TYPE; +import static io.netty.handler.codec.http3.Http3RequestStreamCodecState.NO_STATE; + +/** + * {@link ByteToMessageDecoder} which helps to detect the type of unidirectional stream. + */ +abstract class Http3UnidirectionalStreamInboundHandler extends ByteToMessageDecoder { + private static final AttributeKey REMOTE_CONTROL_STREAM = AttributeKey.valueOf("H3_REMOTE_CONTROL_STREAM"); + private static final AttributeKey REMOTE_QPACK_DECODER_STREAM = + AttributeKey.valueOf("H3_REMOTE_QPACK_DECODER_STREAM"); + private static final AttributeKey REMOTE_QPACK_ENCODER_STREAM = + AttributeKey.valueOf("H3_REMOTE_QPACK_ENCODER_STREAM"); + + final Http3FrameCodecFactory codecFactory; + final Http3ControlStreamInboundHandler localControlStreamHandler; + final Http3ControlStreamOutboundHandler remoteControlStreamHandler; + final Supplier qpackEncoderHandlerFactory; + final Supplier qpackDecoderHandlerFactory; + final LongFunction unknownStreamHandlerFactory; + + Http3UnidirectionalStreamInboundHandler(Http3FrameCodecFactory codecFactory, + Http3ControlStreamInboundHandler localControlStreamHandler, + Http3ControlStreamOutboundHandler remoteControlStreamHandler, + LongFunction unknownStreamHandlerFactory, + Supplier qpackEncoderHandlerFactory, + Supplier qpackDecoderHandlerFactory) { + this.codecFactory = codecFactory; + this.localControlStreamHandler = localControlStreamHandler; + this.remoteControlStreamHandler = remoteControlStreamHandler; + this.qpackEncoderHandlerFactory = qpackEncoderHandlerFactory; + this.qpackDecoderHandlerFactory = qpackDecoderHandlerFactory; + if (unknownStreamHandlerFactory == null) { + // If the user did not specify an own factory just drop all bytes on the floor. + unknownStreamHandlerFactory = type -> ReleaseHandler.INSTANCE; + } + this.unknownStreamHandlerFactory = unknownStreamHandlerFactory; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + if (!in.isReadable()) { + return; + } + int len = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + if (in.readableBytes() < len) { + return; + } + long type = Http3CodecUtils.readVariableLengthInteger(in, len); + switch ((int) type) { + case HTTP3_CONTROL_STREAM_TYPE: + initControlStream(ctx); + break; + case HTTP3_PUSH_STREAM_TYPE: + int pushIdLen = Http3CodecUtils.numBytesForVariableLengthInteger(in.getByte(in.readerIndex())); + if (in.readableBytes() < pushIdLen) { + return; + } + long pushId = Http3CodecUtils.readVariableLengthInteger(in, pushIdLen); + initPushStream(ctx, pushId); + break; + case HTTP3_QPACK_ENCODER_STREAM_TYPE: + // See https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#enc-dec-stream-def + initQpackEncoderStream(ctx); + break; + case HTTP3_QPACK_DECODER_STREAM_TYPE: + // See https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#enc-dec-stream-def + initQpackDecoderStream(ctx); + break; + default: + initUnknownStream(ctx, type); + break; + } + } + + /** + * Called if the current {@link Channel} is a + * control stream. + */ + private void initControlStream(ChannelHandlerContext ctx) { + if (ctx.channel().parent().attr(REMOTE_CONTROL_STREAM).setIfAbsent(true) == null) { + ctx.pipeline().addLast(localControlStreamHandler); + // Replace this handler with the codec now. + ctx.pipeline().replace(this, null, + codecFactory.newCodec(Http3ControlStreamFrameTypeValidator.INSTANCE, NO_STATE, + NO_STATE)); + } else { + // Only one control stream is allowed. + // See https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1 + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_STREAM_CREATION_ERROR, + "Received multiple control streams.", false); + } + } + + private boolean ensureStreamNotExistsYet(ChannelHandlerContext ctx, AttributeKey key) { + return ctx.channel().parent().attr(key).setIfAbsent(true) == null; + } + + /** + * Called if the current {@link Channel} is a + * push stream. + */ + abstract void initPushStream(ChannelHandlerContext ctx, long id); + + /** + * Called if the current {@link Channel} is a + * + * QPACK encoder stream. + */ + private void initQpackEncoderStream(ChannelHandlerContext ctx) { + if (ensureStreamNotExistsYet(ctx, REMOTE_QPACK_ENCODER_STREAM)) { + // Just drop stuff on the floor as we dont support dynamic table atm. + ctx.pipeline().replace(this, null, qpackEncoderHandlerFactory.get()); + } else { + // Only one stream is allowed. + // See https://www.ietf.org/archive/id/draft-ietf-quic-qpack-19.html#section-4.2 + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_STREAM_CREATION_ERROR, + "Received multiple QPACK encoder streams.", false); + } + } + /** + * Called if the current {@link Channel} is a + * + * QPACK decoder stream. + */ + private void initQpackDecoderStream(ChannelHandlerContext ctx) { + if (ensureStreamNotExistsYet(ctx, REMOTE_QPACK_DECODER_STREAM)) { + ctx.pipeline().replace(this, null, qpackDecoderHandlerFactory.get()); + } else { + // Only one stream is allowed. + // See https://www.ietf.org/archive/id/draft-ietf-quic-qpack-19.html#section-4.2 + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_STREAM_CREATION_ERROR, + "Received multiple QPACK decoder streams.", false); + } + } + + /** + * Called if we couldn't detect the stream type of the current {@link Channel}. Let's release everything that + * we receive on this stream. + */ + private void initUnknownStream(ChannelHandlerContext ctx, long streamType) { + ctx.pipeline().replace(this, null, unknownStreamHandlerFactory.apply(streamType)); + } + + static final class ReleaseHandler extends ChannelInboundHandlerAdapter { + static final ReleaseHandler INSTANCE = new ReleaseHandler(); + + @Override + public boolean isSharable() { + return true; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ReferenceCountUtil.release(msg); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundServerHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundServerHandler.java new file mode 100644 index 0000000..63afef0 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundServerHandler.java @@ -0,0 +1,43 @@ +/* + * 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http3.Http3FrameCodec.Http3FrameCodecFactory; + +import java.util.function.LongFunction; +import java.util.function.Supplier; + +final class Http3UnidirectionalStreamInboundServerHandler extends Http3UnidirectionalStreamInboundHandler { + + Http3UnidirectionalStreamInboundServerHandler(Http3FrameCodecFactory codecFactory, + Http3ControlStreamInboundHandler localControlStreamHandler, + Http3ControlStreamOutboundHandler remoteControlStreamHandler, + LongFunction unknownStreamHandlerFactory, + Supplier qpackEncoderHandlerFactory, + Supplier qpackDecoderHandlerFactory) { + super(codecFactory, localControlStreamHandler, remoteControlStreamHandler, unknownStreamHandlerFactory, + qpackEncoderHandlerFactory, qpackDecoderHandlerFactory); + } + + @Override + void initPushStream(ChannelHandlerContext ctx, long id) { + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.H3_STREAM_CREATION_ERROR, + "Server received push stream.", false); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnknownFrame.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnknownFrame.java new file mode 100644 index 0000000..0ebb3e1 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/Http3UnknownFrame.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; + +/** + * Unknown HTTP3 frame. + * These frames are valid on all stream types. + *
+ *    HTTP/3 Frame Format {
+ *      Type (i),
+ *      Length (i),
+ *      Frame Payload (..),
+ *    }
+ * 
+ */ +public interface Http3UnknownFrame extends + Http3RequestStreamFrame, Http3PushStreamFrame, Http3ControlStreamFrame, ByteBufHolder { + + /** + * Return the payload length of the frame. + * + * @return the length. + */ + default long length() { + return content().readableBytes(); + } + + @Override + Http3UnknownFrame copy(); + + @Override + Http3UnknownFrame duplicate(); + + @Override + Http3UnknownFrame retainedDuplicate(); + + @Override + Http3UnknownFrame replace(ByteBuf content); + + @Override + Http3UnknownFrame retain(); + + @Override + Http3UnknownFrame retain(int increment); + + @Override + Http3UnknownFrame touch(); + + @Override + Http3UnknownFrame touch(Object hint); +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/HttpConversionUtil.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/HttpConversionUtil.java new file mode 100644 index 0000000..e42b66c --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/HttpConversionUtil.java @@ -0,0 +1,645 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.UnsupportedValueConverter; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.AsciiString; +import io.netty.util.internal.InternalThreadLocalMap; + +import java.net.URI; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE; +import static io.netty.handler.codec.http.HttpHeaderNames.TE; +import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS; +import static io.netty.handler.codec.http.HttpResponseStatus.parseLine; +import static io.netty.handler.codec.http.HttpScheme.HTTP; +import static io.netty.handler.codec.http.HttpScheme.HTTPS; +import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm; +import static io.netty.handler.codec.http.HttpUtil.isOriginForm; +import static io.netty.util.AsciiString.EMPTY_STRING; +import static io.netty.util.AsciiString.contentEqualsIgnoreCase; +import static io.netty.util.AsciiString.indexOf; +import static io.netty.util.AsciiString.trim; +import static io.netty.util.ByteProcessor.FIND_COMMA; +import static io.netty.util.ByteProcessor.FIND_SEMI_COLON; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.StringUtil.isNullOrEmpty; +import static io.netty.util.internal.StringUtil.length; +import static io.netty.util.internal.StringUtil.unescapeCsvFields; + +/** + * Provides utility methods and constants for the HTTP/3 to HTTP conversion + */ +public final class HttpConversionUtil { + /** + * The set of headers that should not be directly copied when converting headers from HTTP to HTTP/3. + */ + private static final CharSequenceMap HTTP_TO_HTTP3_HEADER_BLACKLIST = + new CharSequenceMap<>(); + static { + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(CONNECTION, EMPTY_STRING); + @SuppressWarnings("deprecation") + AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE; + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING); + @SuppressWarnings("deprecation") + AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION; + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING); + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING); + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING); + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING); + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING); + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING); + HTTP_TO_HTTP3_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING); + } + + /** + * [RFC 7540], 8.1.2.3 states the path must not + * be empty, and instead should be {@code /}. + */ + private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/"); + + private HttpConversionUtil() { + } + + /** + * Provides the HTTP header extensions used to carry HTTP/3 information in HTTP objects + */ + public enum ExtensionHeaderNames { + /** + * HTTP extension header which will identify the stream id from the HTTP/3 event(s) responsible for + * generating an {@code HttpObject} + *

+ * {@code "x-http3-stream-id"} + */ + STREAM_ID("x-http3-stream-id"), + /** + * HTTP extension header which will identify the scheme pseudo header from the HTTP/3 event(s) responsible for + * generating an {@code HttpObject} + *

+ * {@code "x-http3-scheme"} + */ + SCHEME("x-http3-scheme"), + /** + * HTTP extension header which will identify the path pseudo header from the HTTP/3 event(s) responsible for + * generating an {@code HttpObject} + *

+ * {@code "x-http3-path"} + */ + PATH("x-http3-path"), + /** + * HTTP extension header which will identify the stream id used to create this stream in an HTTP/3 push promise + * frame + *

+ * {@code "x-http3-stream-promise-id"} + */ + STREAM_PROMISE_ID("x-http3-stream-promise-id"); + + private final AsciiString text; + + ExtensionHeaderNames(String text) { + this.text = AsciiString.cached(text); + } + + public AsciiString text() { + return text; + } + } + + /** + * Apply HTTP/3 rules while translating status code to {@link HttpResponseStatus} + * + * @param status The status from an HTTP/3 frame + * @return The HTTP/1.x status + * @throws Http3Exception If there is a problem translating from HTTP/3 to HTTP/1.x + */ + private static HttpResponseStatus parseStatus(long streamId, CharSequence status) throws Http3Exception { + HttpResponseStatus result; + try { + result = parseLine(status); + if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) { + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, + "Invalid HTTP/3 status code '" + status + "'", null); + } + } catch (Http3Exception e) { + throw e; + } catch (Throwable t) { + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, "Unrecognized HTTP status code '" + + status + "' encountered in translation to HTTP/1.x" + status, null); + } + return result; + } + + /** + * Create a new object to contain the response data + * + * @param streamId The stream associated with the response + * @param http3Headers The initial set of HTTP/3 headers to create the response with + * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message + * @param validateHttpHeaders

    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @return A new response object which represents headers/data + * @throws Http3Exception + */ + static FullHttpResponse toFullHttpResponse(long streamId, Http3Headers http3Headers, ByteBufAllocator alloc, + boolean validateHttpHeaders) throws Http3Exception { + ByteBuf content = alloc.buffer(); + HttpResponseStatus status = parseStatus(streamId, http3Headers.status()); + // HTTP/3 does not define a way to carry the version or reason phrase that is included in an + // HTTP/1.1 status line. + FullHttpResponse msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content, + validateHttpHeaders); + try { + addHttp3ToHttpHeaders(streamId, http3Headers, msg, false); + } catch (Http3Exception e) { + msg.release(); + throw e; + } catch (Throwable t) { + msg.release(); + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, + "HTTP/3 to HTTP/1.x headers conversion error", t); + } + return msg; + } + + private static CharSequence extractPath(CharSequence method, Http3Headers headers) { + if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) { + // See https://tools.ietf.org/html/rfc7231#section-4.3.6 + return checkNotNull(headers.authority(), + "authority header cannot be null in the conversion to HTTP/1.x"); + } else { + return checkNotNull(headers.path(), + "path header cannot be null in conversion to HTTP/1.x"); + } + } + + /** + * Create a new object to contain the request data + * + * @param streamId The stream associated with the request + * @param http3Headers The initial set of HTTP/3 headers to create the request with + * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message + * @param validateHttpHeaders
    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @return A new request object which represents headers/data + * @throws Http3Exception + */ + static FullHttpRequest toFullHttpRequest(long streamId, Http3Headers http3Headers, ByteBufAllocator alloc, + boolean validateHttpHeaders) throws Http3Exception { + ByteBuf content = alloc.buffer(); + // HTTP/3 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line. + final CharSequence method = checkNotNull(http3Headers.method(), + "method header cannot be null in conversion to HTTP/1.x"); + final CharSequence path = extractPath(method, http3Headers); + FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method + .toString()), path.toString(), content, validateHttpHeaders); + try { + addHttp3ToHttpHeaders(streamId, http3Headers, msg, false); + } catch (Http3Exception e) { + msg.release(); + throw e; + } catch (Throwable t) { + msg.release(); + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, + "HTTP/3 to HTTP/1.x headers conversion error", t); + } + return msg; + } + + /** + * Create a new object to contain the request data. + * + * @param streamId The stream associated with the request + * @param http3Headers The initial set of HTTP/3 headers to create the request with + * @param validateHttpHeaders
    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @return A new request object which represents headers for a chunked request + * @throws Http3Exception + */ + static HttpRequest toHttpRequest(long streamId, Http3Headers http3Headers, boolean validateHttpHeaders) + throws Http3Exception { + // HTTP/3 does not define a way to carry the version identifier that is included in the HTTP/1.1 request line. + final CharSequence method = checkNotNull(http3Headers.method(), + "method header cannot be null in conversion to HTTP/1.x"); + final CharSequence path = extractPath(method, http3Headers); + HttpRequest msg = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method.toString()), + path.toString(), validateHttpHeaders); + try { + addHttp3ToHttpHeaders(streamId, http3Headers, msg.headers(), msg.protocolVersion(), false, true); + } catch (Http3Exception e) { + throw e; + } catch (Throwable t) { + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, + "HTTP/3 to HTTP/1.x headers conversion error", t); + } + return msg; + } + + /** + * Create a new object to contain the response data. + * + * @param streamId The stream associated with the response + * @param http3Headers The initial set of HTTP/3 headers to create the response with + * @param validateHttpHeaders
    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @return A new response object which represents headers for a chunked response + * @throws Http3Exception + */ + static HttpResponse toHttpResponse(final long streamId, + final Http3Headers http3Headers, + final boolean validateHttpHeaders) throws Http3Exception { + final HttpResponseStatus status = parseStatus(streamId, http3Headers.status()); + // HTTP/3 does not define a way to carry the version or reason phrase that is included in an + // HTTP/1.1 status line. + final HttpResponse msg = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders); + try { + addHttp3ToHttpHeaders(streamId, http3Headers, msg.headers(), msg.protocolVersion(), false, false); + } catch (final Http3Exception e) { + throw e; + } catch (final Throwable t) { + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, + "HTTP/3 to HTTP/1.x headers conversion error", t); + } + return msg; + } + + /** + * Translate and add HTTP/3 headers to HTTP/1.x headers. + * + * @param streamId The stream associated with {@code sourceHeaders}. + * @param inputHeaders The HTTP/3 headers to convert. + * @param destinationMessage The object which will contain the resulting HTTP/1.x headers. + * @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers. + * @throws Http3Exception If not all HTTP/3 headers can be translated to HTTP/1.x. + */ + private static void addHttp3ToHttpHeaders(long streamId, Http3Headers inputHeaders, + FullHttpMessage destinationMessage, boolean addToTrailer) throws Http3Exception { + addHttp3ToHttpHeaders(streamId, inputHeaders, + addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(), + destinationMessage.protocolVersion(), addToTrailer, destinationMessage instanceof HttpRequest); + } + + /** + * Translate and add HTTP/3 headers to HTTP/1.x headers. + * + * @param streamId The stream associated with {@code sourceHeaders}. + * @param inputHeaders The HTTP/3 headers to convert. + * @param outputHeaders The object which will contain the resulting HTTP/1.x headers.. + * @param httpVersion What HTTP/1.x version {@code outputHeaders} should be treated as when doing the conversion. + * @param isTrailer {@code true} if {@code outputHeaders} should be treated as trailing headers. + * {@code false} otherwise. + * @param isRequest {@code true} if the {@code outputHeaders} will be used in a request message. + * {@code false} for response message. + * @throws Http3Exception If not all HTTP/3 headers can be translated to HTTP/1.x. + */ + static void addHttp3ToHttpHeaders(long streamId, Http3Headers inputHeaders, HttpHeaders outputHeaders, + HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http3Exception { + Http3ToHttpHeaderTranslator translator = new Http3ToHttpHeaderTranslator(streamId, outputHeaders, isRequest); + try { + translator.translateHeaders(inputHeaders); + } catch (Http3Exception ex) { + throw ex; + } catch (Throwable t) { + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, + "HTTP/3 to HTTP/1.x headers conversion error", t); + } + + outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING); + outputHeaders.remove(HttpHeaderNames.TRAILER); + if (!isTrailer) { + outputHeaders.set(ExtensionHeaderNames.STREAM_ID.text(), streamId); + HttpUtil.setKeepAlive(outputHeaders, httpVersion, true); + } + } + + /** + * Converts the given HTTP/1.x headers into HTTP/3 headers. + * The following headers are only used if they can not be found in from the {@code HOST} header or the + * {@code Request-Line} as defined by rfc7230 + *
    + *
  • {@link ExtensionHeaderNames#SCHEME}
  • + *
+ * {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}. + */ + static Http3Headers toHttp3Headers(HttpMessage in, boolean validateHeaders) { + HttpHeaders inHeaders = in.headers(); + final Http3Headers out = new DefaultHttp3Headers(validateHeaders, inHeaders.size()); + if (in instanceof HttpRequest) { + HttpRequest request = (HttpRequest) in; + URI requestTargetUri = URI.create(request.uri()); + out.path(toHttp3Path(requestTargetUri)); + out.method(request.method().asciiName()); + setHttp3Scheme(inHeaders, requestTargetUri, out); + + // Attempt to take from HOST header before taking from the request-line + String host = inHeaders.getAsString(HttpHeaderNames.HOST); + if (host != null && !host.isEmpty()) { + setHttp3Authority(host, out); + } else { + if (!isOriginForm(request.uri()) && !isAsteriskForm(request.uri())) { + setHttp3Authority(requestTargetUri.getAuthority(), out); + } + } + } else if (in instanceof HttpResponse) { + HttpResponse response = (HttpResponse) in; + out.status(response.status().codeAsText()); + } + + // Add the HTTP headers which have not been consumed above + toHttp3Headers(inHeaders, out); + return out; + } + + static Http3Headers toHttp3Headers(HttpHeaders inHeaders, boolean validateHeaders) { + if (inHeaders.isEmpty()) { + return new DefaultHttp3Headers(); + } + + final Http3Headers out = new DefaultHttp3Headers(validateHeaders, inHeaders.size()); + toHttp3Headers(inHeaders, out); + return out; + } + + private static CharSequenceMap toLowercaseMap(Iterator valuesIter, + int arraySizeHint) { + UnsupportedValueConverter valueConverter = UnsupportedValueConverter.instance(); + CharSequenceMap result = new CharSequenceMap(true, valueConverter, arraySizeHint); + + while (valuesIter.hasNext()) { + AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase(); + try { + int index = lowerCased.forEachByte(FIND_COMMA); + if (index != -1) { + int start = 0; + do { + result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING); + start = index + 1; + } while (start < lowerCased.length() && + (index = lowerCased.forEachByte(start, lowerCased.length() - start, FIND_COMMA)) != -1); + result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING); + } else { + result.add(lowerCased.trim(), EMPTY_STRING); + } + } catch (Exception e) { + // This is not expect to happen because FIND_COMMA never throws but must be caught + // because of the ByteProcessor interface. + throw new IllegalStateException(e); + } + } + return result; + } + + /** + * Filter the {@link HttpHeaderNames#TE} header according to the + * + * special rules in the HTTP/3 RFC. + * @param entry An entry whose name is {@link HttpHeaderNames#TE}. + * @param out the resulting HTTP/3 headers. + */ + private static void toHttp3HeadersFilterTE(Entry entry, + Http3Headers out) { + if (indexOf(entry.getValue(), ',', 0) == -1) { + if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) { + out.add(TE, TRAILERS); + } + } else { + List teValues = unescapeCsvFields(entry.getValue()); + for (CharSequence teValue : teValues) { + if (contentEqualsIgnoreCase(trim(teValue), TRAILERS)) { + out.add(TE, TRAILERS); + break; + } + } + } + } + + static void toHttp3Headers(HttpHeaders inHeaders, Http3Headers out) { + Iterator> iter = inHeaders.iteratorCharSequence(); + // Choose 8 as a default size because it is unlikely we will see more than 4 Connection headers values, but + // still allowing for "enough" space in the map to reduce the chance of hash code collision. + CharSequenceMap connectionBlacklist = + toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8); + while (iter.hasNext()) { + Entry entry = iter.next(); + final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); + if (!HTTP_TO_HTTP3_HEADER_BLACKLIST.contains(aName) && !connectionBlacklist.contains(aName)) { + // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1 makes a special exception + // for TE + if (aName.contentEqualsIgnoreCase(TE)) { + toHttp3HeadersFilterTE(entry, out); + } else if (aName.contentEqualsIgnoreCase(COOKIE)) { + AsciiString value = AsciiString.of(entry.getValue()); + // split up cookies to allow for better compression + try { + int index = value.forEachByte(FIND_SEMI_COLON); + if (index != -1) { + int start = 0; + do { + out.add(COOKIE, value.subSequence(start, index, false)); + // skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1) + start = index + 2; + } while (start < value.length() && + (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1); + if (start >= value.length()) { + throw new IllegalArgumentException("cookie value is of unexpected format: " + value); + } + out.add(COOKIE, value.subSequence(start, value.length(), false)); + } else { + out.add(COOKIE, value); + } + } catch (Exception e) { + // This is not expect to happen because FIND_SEMI_COLON never throws but must be caught + // because of the ByteProcessor interface. + throw new IllegalStateException(e); + } + } else { + out.add(aName, entry.getValue()); + } + } + } + } + + /** + * Generate an HTTP/3 {code :path} from a URI in accordance with + * HTTP3 spec. + */ + private static AsciiString toHttp3Path(URI uri) { + StringBuilder pathBuilder = new StringBuilder(length(uri.getRawPath()) + + length(uri.getRawQuery()) + length(uri.getRawFragment()) + 2); + if (!isNullOrEmpty(uri.getRawPath())) { + pathBuilder.append(uri.getRawPath()); + } + if (!isNullOrEmpty(uri.getRawQuery())) { + pathBuilder.append('?'); + pathBuilder.append(uri.getRawQuery()); + } + if (!isNullOrEmpty(uri.getRawFragment())) { + pathBuilder.append('#'); + pathBuilder.append(uri.getRawFragment()); + } + String path = pathBuilder.toString(); + return path.isEmpty() ? EMPTY_REQUEST_PATH : new AsciiString(path); + } + + // package-private for testing only + static void setHttp3Authority(String authority, Http3Headers out) { + // The authority MUST NOT include the deprecated "userinfo" subcomponent + if (authority != null) { + if (authority.isEmpty()) { + out.authority(EMPTY_STRING); + } else { + int start = authority.indexOf('@') + 1; + int length = authority.length() - start; + if (length == 0) { + throw new IllegalArgumentException("authority: " + authority); + } + out.authority(new AsciiString(authority, start, length)); + } + } + } + + private static void setHttp3Scheme(HttpHeaders in, URI uri, Http3Headers out) { + String value = uri.getScheme(); + if (value != null) { + out.scheme(new AsciiString(value)); + return; + } + + // Consume the Scheme extension header if present + CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text()); + if (cValue != null) { + out.scheme(AsciiString.of(cValue)); + return; + } + + if (uri.getPort() == HTTPS.port()) { + out.scheme(HTTPS.name()); + } else if (uri.getPort() == HTTP.port()) { + out.scheme(HTTP.name()); + } else { + throw new IllegalArgumentException(":scheme must be specified. " + + "see https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1"); + } + } + + /** + * Utility which translates HTTP/3 headers to HTTP/1 headers. + */ + private static final class Http3ToHttpHeaderTranslator { + /** + * Translations from HTTP/3 header name to the HTTP/1.x equivalent. + */ + private static final CharSequenceMap + REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap(); + private static final CharSequenceMap + RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap(); + static { + RESPONSE_HEADER_TRANSLATIONS.add(Http3Headers.PseudoHeaderName.AUTHORITY.value(), + HttpHeaderNames.HOST); + RESPONSE_HEADER_TRANSLATIONS.add(Http3Headers.PseudoHeaderName.SCHEME.value(), + ExtensionHeaderNames.SCHEME.text()); + REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS); + RESPONSE_HEADER_TRANSLATIONS.add(Http3Headers.PseudoHeaderName.PATH.value(), + ExtensionHeaderNames.PATH.text()); + } + + private final long streamId; + private final HttpHeaders output; + private final CharSequenceMap translations; + + /** + * Create a new instance + * + * @param output The HTTP/1.x headers object to store the results of the translation + * @param request if {@code true}, translates headers using the request translation map. Otherwise uses the + * response translation map. + */ + Http3ToHttpHeaderTranslator(long streamId, HttpHeaders output, boolean request) { + this.streamId = streamId; + this.output = output; + translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; + } + + void translateHeaders(Iterable> inputHeaders) throws Http3Exception { + // lazily created as needed + StringBuilder cookies = null; + + for (Entry entry : inputHeaders) { + final CharSequence name = entry.getKey(); + final CharSequence value = entry.getValue(); + AsciiString translatedName = translations.get(name); + if (translatedName != null) { + output.add(translatedName, AsciiString.of(value)); + } else if (!Http3Headers.PseudoHeaderName.isPseudoHeader(name)) { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 + // All headers that start with ':' are only valid in HTTP/3 context + if (name.length() == 0 || name.charAt(0) == ':') { + throw streamError(streamId, Http3ErrorCode.H3_MESSAGE_ERROR, + "Invalid HTTP/3 header '" + name + "' encountered in translation to HTTP/1.x", + null); + } + if (COOKIE.equals(name)) { + // combine the cookie values into 1 header entry. + // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 + if (cookies == null) { + cookies = InternalThreadLocalMap.get().stringBuilder(); + } else if (cookies.length() > 0) { + cookies.append("; "); + } + cookies.append(value); + } else { + output.add(name, value); + } + } + } + if (cookies != null) { + output.add(COOKIE, cookies.toString()); + } + } + } + + private static Http3Exception streamError(long streamId, Http3ErrorCode error, String msg, Throwable cause) { + return new Http3Exception(error, streamId + ": " + msg, cause); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackAttributes.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackAttributes.java new file mode 100644 index 0000000..10a6525 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackAttributes.java @@ -0,0 +1,108 @@ +/* + * 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.handler.codec.http3; + +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; + +import static java.util.Objects.requireNonNull; + +final class QpackAttributes { + private final QuicChannel channel; + private final boolean dynamicTableDisabled; + private final Promise encoderStreamPromise; + private final Promise decoderStreamPromise; + + private QuicStreamChannel encoderStream; + private QuicStreamChannel decoderStream; + + QpackAttributes(QuicChannel channel, boolean disableDynamicTable) { + this.channel = channel; + dynamicTableDisabled = disableDynamicTable; + encoderStreamPromise = dynamicTableDisabled ? null : channel.eventLoop().newPromise(); + decoderStreamPromise = dynamicTableDisabled ? null : channel.eventLoop().newPromise(); + } + + boolean dynamicTableDisabled() { + return dynamicTableDisabled; + } + + boolean decoderStreamAvailable() { + return !dynamicTableDisabled && decoderStream != null; + } + + boolean encoderStreamAvailable() { + return !dynamicTableDisabled && encoderStream != null; + } + + void whenEncoderStreamAvailable(GenericFutureListener> listener) { + assert !dynamicTableDisabled; + assert encoderStreamPromise != null; + encoderStreamPromise.addListener(listener); + } + + void whenDecoderStreamAvailable(GenericFutureListener> listener) { + assert !dynamicTableDisabled; + assert decoderStreamPromise != null; + decoderStreamPromise.addListener(listener); + } + + QuicStreamChannel decoderStream() { + assert decoderStreamAvailable(); + return decoderStream; + } + + QuicStreamChannel encoderStream() { + assert encoderStreamAvailable(); + return encoderStream; + } + + void decoderStream(QuicStreamChannel decoderStream) { + assert channel.eventLoop().inEventLoop(); + assert !dynamicTableDisabled; + assert decoderStreamPromise != null; + assert this.decoderStream == null; + this.decoderStream = requireNonNull(decoderStream); + decoderStreamPromise.setSuccess(decoderStream); + } + + void encoderStream(QuicStreamChannel encoderStream) { + assert channel.eventLoop().inEventLoop(); + assert !dynamicTableDisabled; + assert encoderStreamPromise != null; + assert this.encoderStream == null; + this.encoderStream = requireNonNull(encoderStream); + encoderStreamPromise.setSuccess(encoderStream); + } + + void encoderStreamInactive(Throwable cause) { + assert channel.eventLoop().inEventLoop(); + assert !dynamicTableDisabled; + assert encoderStreamPromise != null; + encoderStreamPromise.tryFailure(cause); + } + + void decoderStreamInactive(Throwable cause) { + assert channel.eventLoop().inEventLoop(); + assert !dynamicTableDisabled; + assert decoderStreamPromise != null; + decoderStreamPromise.tryFailure(cause); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoder.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoder.java new file mode 100644 index 0000000..6539f45 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoder.java @@ -0,0 +1,516 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.AsciiString; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import static io.netty.handler.codec.http3.Http3CodecUtils.closeOnFailure; +import static io.netty.handler.codec.http3.QpackDecoderStateSyncStrategy.ackEachInsert; +import static io.netty.handler.codec.http3.QpackUtil.decodePrefixedIntegerAsInt; +import static io.netty.handler.codec.http3.QpackUtil.encodePrefixedInteger; +import static io.netty.handler.codec.http3.QpackUtil.firstByteEquals; +import static io.netty.handler.codec.http3.QpackUtil.toIntOrThrow; +import static java.lang.Math.floorDiv; + +final class QpackDecoder { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QpackDecoder.class); + private static final QpackException DYNAMIC_TABLE_CAPACITY_EXCEEDS_MAX = + QpackException.newStatic(QpackDecoder.class, "setDynamicTableCapacity(...)", + "QPACK - decoder dynamic table capacity exceeds max capacity."); + private static final QpackException HEADER_ILLEGAL_INDEX_VALUE = + QpackException.newStatic(QpackDecoder.class, "decodeIndexed(...)", "QPACK - illegal index value"); + private static final QpackException NAME_ILLEGAL_INDEX_VALUE = + QpackException.newStatic(QpackDecoder.class, "decodeLiteralWithNameRef(...)", + "QPACK - illegal name index value"); + private static final QpackException INVALID_REQUIRED_INSERT_COUNT = + QpackException.newStatic(QpackDecoder.class, "decodeRequiredInsertCount(...)", + "QPACK - invalid required insert count"); + private static final QpackException MAX_BLOCKED_STREAMS_EXCEEDED = + QpackException.newStatic(QpackDecoder.class, "shouldWaitForDynamicTableUpdates(...)", + "QPACK - exceeded max blocked streams"); + private static final QpackException BLOCKED_STREAM_RESUMPTION_FAILED = + QpackException.newStatic(QpackDecoder.class, "sendInsertCountIncrementIfRequired(...)", + "QPACK - failed to resume a blocked stream"); + + private static final QpackException UNKNOWN_TYPE = + QpackException.newStatic(QpackDecoder.class, "decode(...)", "QPACK - unknown type"); + + private final QpackHuffmanDecoder huffmanDecoder; + private final QpackDecoderDynamicTable dynamicTable; + private final long maxTableCapacity; + private final int maxBlockedStreams; + private final QpackDecoderStateSyncStrategy stateSyncStrategy; + /** + * Hashmap with key as the required insert count to unblock the stream and the value a {@link List} of + * {@link Runnable} to invoke when the stream can be unblocked. + */ + private final IntObjectHashMap> blockedStreams; + + private final long maxEntries; + private final long fullRange; + private int blockedStreamsCount; + private long lastAckInsertCount; + + QpackDecoder(long maxTableCapacity, int maxBlockedStreams) { + this(maxTableCapacity, maxBlockedStreams, new QpackDecoderDynamicTable(), ackEachInsert()); + } + + QpackDecoder(long maxTableCapacity, int maxBlockedStreams, + QpackDecoderDynamicTable dynamicTable, QpackDecoderStateSyncStrategy stateSyncStrategy) { + huffmanDecoder = new QpackHuffmanDecoder(); + this.maxTableCapacity = maxTableCapacity; + this.maxBlockedStreams = maxBlockedStreams; + this.stateSyncStrategy = stateSyncStrategy; + blockedStreams = new IntObjectHashMap<>(Math.min(16, maxBlockedStreams)); + this.dynamicTable = dynamicTable; + maxEntries = QpackUtil.maxEntries(maxTableCapacity); + try { + fullRange = toIntOrThrow(2 * maxEntries); + } catch (QpackException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Decode the header block and add these to the {@link BiConsumer}. This method assumes the entire header block is + * contained in {@code in}. However, this method may not be able to decode the header block if the QPACK dynamic + * table does not contain all entries required to decode the header block. + * See blocked streams. + * In such a case, this method will return {@code false} and would invoke {@code whenDecoded} when the stream is + * unblocked and the header block is completely decoded. + * + * @param qpackAttributes {@link QpackAttributes} for the channel. + * @param streamId for the stream on which this header block was received. + * @param in {@link ByteBuf} containing the header block. + * @param length Number of bytes to be read from {@code in} + * @param sink {@link BiConsumer} to + * @param whenDecoded {@link Runnable} to invoke when a blocked decode finishes decoding. + * @return {@code true} if the headers were decoded. + */ + public boolean decode(QpackAttributes qpackAttributes, long streamId, ByteBuf in, + int length, BiConsumer sink, Runnable whenDecoded) + throws QpackException { + final int initialReaderIdx = in.readerIndex(); + final int requiredInsertCount = decodeRequiredInsertCount(qpackAttributes, in); + if (shouldWaitForDynamicTableUpdates(requiredInsertCount)) { + blockedStreamsCount++; + blockedStreams.computeIfAbsent(requiredInsertCount, __ -> new ArrayList<>(2)).add(whenDecoded); + in.readerIndex(initialReaderIdx); + return false; + } + + in = in.readSlice(length - (in.readerIndex() - initialReaderIdx)); + final int base = decodeBase(in, requiredInsertCount); + + while (in.isReadable()) { + byte b = in.getByte(in.readerIndex()); + if (isIndexed(b)) { + decodeIndexed(in, sink, base); + } else if (isIndexedWithPostBase(b)) { + decodeIndexedWithPostBase(in, sink, base); + } else if (isLiteralWithNameRef(b)) { + decodeLiteralWithNameRef(in, sink, base); + } else if (isLiteralWithPostBaseNameRef(b)) { + decodeLiteralWithPostBaseNameRef(in, sink, base); + } else if (isLiteral(b)) { + decodeLiteral(in, sink); + } else { + throw UNKNOWN_TYPE; + } + } + if (requiredInsertCount > 0) { + assert !qpackAttributes.dynamicTableDisabled(); + assert qpackAttributes.decoderStreamAvailable(); + + stateSyncStrategy.sectionAcknowledged(requiredInsertCount); + final ByteBuf sectionAck = qpackAttributes.decoderStream().alloc().buffer(8); + encodePrefixedInteger(sectionAck, (byte) 0b1000_0000, 7, streamId); + closeOnFailure(qpackAttributes.decoderStream().writeAndFlush(sectionAck)); + } + return true; + } + + /** + * Updates dynamic table capacity corresponding to the + * + * encoder instruction. + * + * @param capacity New capacity. + * @throws QpackException If the capacity update fails. + */ + void setDynamicTableCapacity(long capacity) throws QpackException { + if (capacity > maxTableCapacity) { + throw DYNAMIC_TABLE_CAPACITY_EXCEEDS_MAX; + } + dynamicTable.setCapacity(capacity); + } + + /** + * Inserts a header field with a name reference corresponding to the + * + * encoder instruction. + * + * @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream. + * @param staticTableRef {@code true} if the name reference is to the static table, {@code false} if the reference + * is to the dynamic table. + * @param nameIdx Index of the name in the table. + * @param value Literal value. + * @throws QpackException if the insertion fails. + */ + void insertWithNameReference(QuicStreamChannel qpackDecoderStream, boolean staticTableRef, int nameIdx, + CharSequence value) throws QpackException { + final QpackHeaderField entryForName; + if (staticTableRef) { + entryForName = QpackStaticTable.getField(nameIdx); + } else { + entryForName = dynamicTable.getEntryRelativeEncoderInstructions(nameIdx); + } + dynamicTable.add(new QpackHeaderField(entryForName.name, value)); + sendInsertCountIncrementIfRequired(qpackDecoderStream); + } + + /** + * Inserts a header field with a literal name corresponding to the + * + * encoder instruction. + * + * @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream. + * @param name of the field. + * @param value of the field. + * @throws QpackException if the insertion fails. + */ + void insertLiteral(QuicStreamChannel qpackDecoderStream, CharSequence name, CharSequence value) + throws QpackException { + dynamicTable.add(new QpackHeaderField(name, value)); + sendInsertCountIncrementIfRequired(qpackDecoderStream); + } + + /** + * Duplicates a previous entry corresponding to the + * + * encoder instruction. + * + * @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream. + * @param index which is duplicated. + * @throws QpackException if duplication fails. + */ + void duplicate(QuicStreamChannel qpackDecoderStream, int index) + throws QpackException { + dynamicTable.add(dynamicTable.getEntryRelativeEncoderInstructions(index)); + sendInsertCountIncrementIfRequired(qpackDecoderStream); + } + + /** + * Callback when a bi-directional stream is + * abandoned + * + * @param qpackDecoderStream {@link QuicStreamChannel} for the QPACK decoder stream. + * @param streamId which is abandoned. + */ + void streamAbandoned(QuicStreamChannel qpackDecoderStream, long streamId) { + if (maxTableCapacity == 0) { + return; + } + // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.4.2 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Stream ID (6+) | + // +---+---+-----------------------+ + final ByteBuf cancel = qpackDecoderStream.alloc().buffer(8); + encodePrefixedInteger(cancel, (byte) 0b0100_0000, 6, streamId); + closeOnFailure(qpackDecoderStream.writeAndFlush(cancel)); + } + + private static boolean isIndexed(byte b) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---+-----------------------+ + return (b & 0b1000_0000) == 0b1000_0000; + } + + private static boolean isLiteralWithNameRef(byte b) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T |Name Index (4+)| + // +---+---+---+---+---------------+ + return (b & 0b1100_0000) == 0b0100_0000; + } + + private static boolean isLiteral(byte b) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-lit + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | N | H |NameLen(3+)| + // +---+---+---+---+---+-----------+ + return (b & 0b1110_0000) == 0b0010_0000; + } + + private static boolean isIndexedWithPostBase(byte b) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line-with-pos + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+---+---+---------------+ + return (b & 0b1111_0000) == 0b0001_0000; + } + + private static boolean isLiteralWithPostBaseNameRef(byte b) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-pos + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---+-----------+ + return (b & 0b1111_0000) == 0b0000_0000; + } + + private void decodeIndexed(ByteBuf in, BiConsumer sink, int base) + throws QpackException { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---+-----------------------+ + // + // T == 1 implies static table + final QpackHeaderField field; + if (firstByteEquals(in, (byte) 0b1100_0000)) { + final int idx = decodePrefixedIntegerAsInt(in, 6); + assert idx >= 0; + if (idx >= QpackStaticTable.length) { + throw HEADER_ILLEGAL_INDEX_VALUE; + } + field = QpackStaticTable.getField(idx); + } else { + final int idx = decodePrefixedIntegerAsInt(in, 6); + assert idx >= 0; + field = dynamicTable.getEntryRelativeEncodedField(base - idx - 1); + } + sink.accept(field.name, field.value); + } + + private void decodeIndexedWithPostBase(ByteBuf in, BiConsumer sink, int base) + throws QpackException { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line-with-pos + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+---+---+---------------+ + final int idx = decodePrefixedIntegerAsInt(in, 4); + assert idx >= 0; + QpackHeaderField field = dynamicTable.getEntryRelativeEncodedField(base + idx); + sink.accept(field.name, field.value); + } + + private void decodeLiteralWithNameRef(ByteBuf in, BiConsumer sink, int base) + throws QpackException { + final CharSequence name; + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T |Name Index (4+)| + // +---+---+---+---+---------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // + // T == 1 implies static table + if (firstByteEquals(in, (byte) 0b0001_0000)) { + final int idx = decodePrefixedIntegerAsInt(in, 4); + assert idx >= 0; + if (idx >= QpackStaticTable.length) { + throw NAME_ILLEGAL_INDEX_VALUE; + } + name = QpackStaticTable.getField(idx).name; + } else { + final int idx = decodePrefixedIntegerAsInt(in, 4); + assert idx >= 0; + name = dynamicTable.getEntryRelativeEncodedField(base - idx - 1).name; + } + final CharSequence value = decodeHuffmanEncodedLiteral(in, 7); + sink.accept(name, value); + } + + private void decodeLiteralWithPostBaseNameRef(ByteBuf in, BiConsumer sink, int base) + throws QpackException { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---+-----------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + final int idx = decodePrefixedIntegerAsInt(in, 3); + assert idx >= 0; + CharSequence name = dynamicTable.getEntryRelativeEncodedField(base + idx).name; + final CharSequence value = decodeHuffmanEncodedLiteral(in, 7); + sink.accept(name, value); + } + + private void decodeLiteral(ByteBuf in, BiConsumer sink) throws QpackException { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-lit + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | N | H |NameLen(3+)| + // +---+---+---+---+---+-----------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + final CharSequence name = decodeHuffmanEncodedLiteral(in, 3); + final CharSequence value = decodeHuffmanEncodedLiteral(in, 7); + sink.accept(name, value); + } + + private CharSequence decodeHuffmanEncodedLiteral(ByteBuf in, int prefix) throws QpackException { + assert prefix < 8; + final boolean huffmanEncoded = firstByteEquals(in, (byte) (1 << prefix)); + final int length = decodePrefixedIntegerAsInt(in, prefix); + assert length >= 0; + if (huffmanEncoded) { + return huffmanDecoder.decode(in, length); + } + byte[] buf = new byte[length]; + in.readBytes(buf); + return new AsciiString(buf, false); + } + + // Visible for testing + int decodeRequiredInsertCount(QpackAttributes qpackAttributes, ByteBuf buf) throws QpackException { + final long encodedInsertCount = QpackUtil.decodePrefixedInteger(buf, 8); + assert encodedInsertCount >= 0; + // https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count + // FullRange = 2 * MaxEntries + // if EncodedInsertCount == 0: + // ReqInsertCount = 0 + // else: + // if EncodedInsertCount > FullRange: + // Error + // MaxValue = TotalNumberOfInserts + MaxEntries + // + // # MaxWrapped is the largest possible value of + // # ReqInsertCount that is 0 mod 2 * MaxEntries + // MaxWrapped = floor(MaxValue / FullRange) * FullRange + // ReqInsertCount = MaxWrapped + EncodedInsertCount - 1 + // + // # If ReqInsertCount exceeds MaxValue, the Encoder's value + // # must have wrapped one fewer time + // if ReqInsertCount > MaxValue: + // if ReqInsertCount <= FullRange: + // Error + // ReqInsertCount -= FullRange + // + // # Value of 0 must be encoded as 0. + // if ReqInsertCount == 0: + // Error + if (encodedInsertCount == 0) { + return 0; + } + if (qpackAttributes.dynamicTableDisabled() || encodedInsertCount > fullRange) { + throw INVALID_REQUIRED_INSERT_COUNT; + } + + final long maxValue = dynamicTable.insertCount() + maxEntries; + final long maxWrapped = floorDiv(maxValue, fullRange) * fullRange; + long requiredInsertCount = maxWrapped + encodedInsertCount - 1; + + if (requiredInsertCount > maxValue) { + if (requiredInsertCount <= fullRange) { + throw INVALID_REQUIRED_INSERT_COUNT; + } + requiredInsertCount -= fullRange; + } + // requiredInsertCount can not be negative as encodedInsertCount read from the buffer can not be negative. + if (requiredInsertCount == 0) { + throw INVALID_REQUIRED_INSERT_COUNT; + } + return toIntOrThrow(requiredInsertCount); + } + + // Visible for testing + int decodeBase(ByteBuf buf, int requiredInsertCount) throws QpackException { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-encoded-field-section-prefi + // 0 1 2 3 4 5 6 7 + // +---+---------------------------+ + // | S | Delta Base (7+) | + // +---+---------------------------+ + final boolean s = (buf.getByte(buf.readerIndex()) & 0b1000_0000) == 0b1000_0000; + final int deltaBase = decodePrefixedIntegerAsInt(buf, 7); + assert deltaBase >= 0; + // https://www.rfc-editor.org/rfc/rfc9204.html#name-base + // if S == 0: + // Base = ReqInsertCount + DeltaBase + // else: + // Base = ReqInsertCount - DeltaBase - 1 + return s ? requiredInsertCount - deltaBase - 1 : requiredInsertCount + deltaBase; + } + + private boolean shouldWaitForDynamicTableUpdates(int requiredInsertCount) throws QpackException { + if (requiredInsertCount > dynamicTable.insertCount()) { + if (blockedStreamsCount == maxBlockedStreams - 1) { + throw MAX_BLOCKED_STREAMS_EXCEEDED; + } + return true; + } + return false; + } + + private void sendInsertCountIncrementIfRequired(QuicStreamChannel qpackDecoderStream) throws QpackException { + final int insertCount = dynamicTable.insertCount(); + final List runnables = this.blockedStreams.get(insertCount); + if (runnables != null) { + boolean failed = false; + for (Runnable runnable : runnables) { + try { + runnable.run(); + } catch (Exception e) { + failed = true; + logger.error("Failed to resume a blocked stream {}.", runnable, e); + } + } + if (failed) { + throw BLOCKED_STREAM_RESUMPTION_FAILED; + } + } + if (stateSyncStrategy.entryAdded(insertCount)) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-count-increment + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | Increment (6+) | + // +---+---+-----------------------+ + final ByteBuf incr = qpackDecoderStream.alloc().buffer(8); + encodePrefixedInteger(incr, (byte) 0b0, 6, insertCount - lastAckInsertCount); + lastAckInsertCount = insertCount; + closeOnFailure(qpackDecoderStream.writeAndFlush(incr)); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderDynamicTable.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderDynamicTable.java new file mode 100644 index 0000000..eba03fc --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderDynamicTable.java @@ -0,0 +1,170 @@ +/* + * 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.handler.codec.http3; + +import java.util.Arrays; + +import static io.netty.handler.codec.http3.QpackHeaderField.ENTRY_OVERHEAD; +import static io.netty.handler.codec.http3.QpackUtil.MAX_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http3.QpackUtil.MIN_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http3.QpackUtil.toIntOrThrow; +import static java.lang.Math.floorDiv; + +final class QpackDecoderDynamicTable { + private static final QpackException GET_ENTRY_ILLEGAL_INDEX_VALUE = + QpackException.newStatic(QpackDecoderDynamicTable.class, "getEntry(...)", + "QPACK - illegal decoder dynamic table index value"); + private static final QpackException HEADER_TOO_LARGE = + QpackException.newStatic(QpackDecoderDynamicTable.class, "add(...)", "QPACK - header entry too large."); + + // a circular queue of header fields + private QpackHeaderField[] fields; + private int head; + private int tail; + private long size; + private long capacity = -1; // ensure setCapacity creates the array + private int insertCount; + + int length() { + return head < tail ? fields.length - tail + head : head - tail; + } + + long size() { + return size; + } + + int insertCount() { + return insertCount; + } + + QpackHeaderField getEntry(int index) throws QpackException { + if (index < 0 || fields == null || index >= fields.length) { + throw GET_ENTRY_ILLEGAL_INDEX_VALUE; + } + QpackHeaderField entry = fields[index]; + if (entry == null) { + throw GET_ENTRY_ILLEGAL_INDEX_VALUE; + } + return entry; + } + + QpackHeaderField getEntryRelativeEncodedField(int index) throws QpackException { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-relative-indexing + return getEntry(moduloIndex(index)); + } + + QpackHeaderField getEntryRelativeEncoderInstructions(int index) throws QpackException { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-relative-indexing + // Name index is the relative index, relative to the last added entry. + return getEntry(index > tail ? fields.length - index + tail : tail - index); + } + + void add(QpackHeaderField header) throws QpackException { + long headerSize = header.size(); + if (headerSize > capacity) { + throw HEADER_TOO_LARGE; + } + while (capacity - size < headerSize) { + remove(); + } + insertCount++; + fields[getAndIncrementHead()] = header; + size += headerSize; + } + + private void remove() { + QpackHeaderField removed = fields[tail]; + if (removed == null) { + return; + } + size -= removed.size(); + fields[getAndIncrementTail()] = null; + } + + void clear() { + if (fields != null) { + Arrays.fill(fields, null); + } + head = 0; + tail = 0; + size = 0; + } + + void setCapacity(long capacity) throws QpackException { + if (capacity < MIN_HEADER_TABLE_SIZE || capacity > MAX_HEADER_TABLE_SIZE) { + throw new IllegalArgumentException("capacity is invalid: " + capacity); + } + // initially capacity will be -1 so init won't return here + if (this.capacity == capacity) { + return; + } + this.capacity = capacity; + + if (capacity == 0) { + clear(); + } else { + // initially size will be 0 so remove won't be called + while (size > capacity) { + remove(); + } + } + + int maxEntries = toIntOrThrow(2 * floorDiv(capacity, ENTRY_OVERHEAD)); + + // check if capacity change requires us to reallocate the array + if (fields != null && fields.length == maxEntries) { + return; + } + + QpackHeaderField[] tmp = new QpackHeaderField[maxEntries]; + + // initially length will be 0 so there will be no copy + int len = length(); + if (fields != null && tail != head) { + if (head > tail) { + System.arraycopy(fields, tail, tmp, 0, head - tail); + } else { + System.arraycopy(fields, 0, tmp, 0, head); + System.arraycopy(fields, tail, tmp, head, fields.length - tail); + } + } + + tail = 0; + head = tail + len; + fields = tmp; + } + + private int getAndIncrementHead() { + int val = this.head; + this.head = safeIncrementIndex(val); + return val; + } + + private int getAndIncrementTail() { + int val = this.tail; + this.tail = safeIncrementIndex(val); + return val; + } + + private int safeIncrementIndex(int index) { + return ++index % fields.length; + } + + private int moduloIndex(int index) { + return fields == null ? index : index % fields.length; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderHandler.java new file mode 100644 index 0000000..7d0d48f --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderHandler.java @@ -0,0 +1,152 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +import static io.netty.handler.codec.http3.Http3CodecUtils.connectionError; +import static io.netty.handler.codec.http3.Http3ErrorCode.QPACK_DECODER_STREAM_ERROR; +import static io.netty.handler.codec.http3.QpackUtil.decodePrefixedIntegerAsInt; + +final class QpackDecoderHandler extends ByteToMessageDecoder { + + private boolean discard; + private final QpackEncoder qpackEncoder; + + QpackDecoderHandler(QpackEncoder qpackEncoder) { + this.qpackEncoder = qpackEncoder; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + if (!in.isReadable()) { + return; + } + if (discard) { + in.skipBytes(in.readableBytes()); + return; + } + + byte b = in.getByte(in.readerIndex()); + + // 4.4.1. Section Acknowledgment + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Stream ID (7+) | + // +---+---------------------------+ + if ((b & 0b1000_0000) == 0b1000_0000) { + long streamId = QpackUtil.decodePrefixedInteger(in, 7); + if (streamId < 0) { + // Not enough readable bytes + return; + } + try { + qpackEncoder.sectionAcknowledgment(streamId); + } catch (QpackException e) { + connectionError(ctx, new Http3Exception(QPACK_DECODER_STREAM_ERROR, + "Section acknowledgment decode failed.", e), true); + } + return; + } + + // 4.4.2. Stream Cancellation + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Stream ID (6+) | + // +---+---+-----------------------+ + if ((b & 0b1100_0000) == 0b0100_0000) { + long streamId = QpackUtil.decodePrefixedInteger(in, 6); + if (streamId < 0) { + // Not enough readable bytes + return; + } + try { + qpackEncoder.streamCancellation(streamId); + } catch (QpackException e) { + connectionError(ctx, new Http3Exception(QPACK_DECODER_STREAM_ERROR, + "Stream cancellation decode failed.", e), true); + } + return; + } + + // 4.4.3. Insert Count Increment + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | Increment (6+) | + // +---+---+-----------------------+ + if ((b & 0b1100_0000) == 0b0000_0000) { + int increment = decodePrefixedIntegerAsInt(in, 6); + if (increment == 0) { + discard = true; + // Zero is not allowed as an increment + // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-count-increment + // Increment is an unsigned integer, so only 0 is the invalid value. + // https://www.rfc-editor.org/rfc/rfc7541#section-5 + connectionError(ctx, QPACK_DECODER_STREAM_ERROR, + "Invalid increment '" + increment + "'.", false); + return; + } + if (increment < 0) { + // Not enough readable bytes + return; + } + try { + qpackEncoder.insertCountIncrement(increment); + } catch (QpackException e) { + connectionError(ctx, new Http3Exception(QPACK_DECODER_STREAM_ERROR, + "Insert count increment decode failed.", e), true); + } + return; + } + // unknown frame + discard = true; + connectionError(ctx, QPACK_DECODER_STREAM_ERROR, + "Unknown decoder instruction '" + b + "'.", false); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.fireChannelReadComplete(); + + // QPACK streams should always be processed, no matter what the user is doing in terms of configuration + // and AUTO_READ. + Http3CodecUtils.readIfNoAutoRead(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof ChannelInputShutdownEvent) { + // See https://www.rfc-editor.org/rfc/rfc9204.html#name-encoder-and-decoder-streams + Http3CodecUtils.criticalStreamClosed(ctx); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + // See https://www.rfc-editor.org/rfc/rfc9204.html#name-encoder-and-decoder-streams + Http3CodecUtils.criticalStreamClosed(ctx); + ctx.fireChannelInactive(); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderStateSyncStrategy.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderStateSyncStrategy.java new file mode 100644 index 0000000..dd7752a --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackDecoderStateSyncStrategy.java @@ -0,0 +1,75 @@ +/* + * 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.handler.codec.http3; + +/** + * A strategy that determines when to send acknowledgment of new table + * entries on the QPACK decoder stream. + */ +public interface QpackDecoderStateSyncStrategy { + + /** + * Callback when an + * encoded header field section is decoded successfully by the decoder. + * + * @param requiredInsertCount for the encoded field section. + */ + void sectionAcknowledged(int requiredInsertCount); + + /** + * When a header field entry is added to the decoder dynamic table. + * + * @param insertCount for the entry. + * @return {@code true} if an insert count + * increment decoder instruction should be sent. + */ + boolean entryAdded(int insertCount); + + /** + * Returns a {@link QpackDecoderStateSyncStrategy} that will acknowledge each entry added via + * {@link #entryAdded(int)} unless a prior {@link #sectionAcknowledged(int)} call has implicitly acknowledged the + * addition. + * + * @return A {@link QpackDecoderStateSyncStrategy} that will acknowledge each entry added via + * {@link #entryAdded(int)} unless a prior {@link #sectionAcknowledged(int)} call has implicitly acknowledged the + * addition. + */ + static QpackDecoderStateSyncStrategy ackEachInsert() { + return new QpackDecoderStateSyncStrategy() { + private int lastCountAcknowledged; + + @Override + public void sectionAcknowledged(int requiredInsertCount) { + if (lastCountAcknowledged < requiredInsertCount) { + lastCountAcknowledged = requiredInsertCount; + } + } + + @Override + public boolean entryAdded(int insertCount) { + if (lastCountAcknowledged < insertCount) { + lastCountAcknowledged = insertCount; + return true; + } + return false; + } + }; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoder.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoder.java new file mode 100644 index 0000000..79b2714 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoder.java @@ -0,0 +1,544 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.collection.LongObjectHashMap; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Map; +import java.util.Queue; + +import static io.netty.handler.codec.http3.Http3CodecUtils.closeOnFailure; +import static io.netty.handler.codec.http3.QpackHeaderField.sizeOf; +import static io.netty.handler.codec.http3.QpackUtil.encodePrefixedInteger; + +/** + * A QPACK encoder. + */ +final class QpackEncoder { + private static final QpackException INVALID_SECTION_ACKNOWLEDGMENT = + QpackException.newStatic(QpackDecoder.class, "sectionAcknowledgment(...)", + "QPACK - section acknowledgment received for unknown stream."); + private static final int DYNAMIC_TABLE_ENCODE_NOT_DONE = -1; + private static final int DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE = -2; + + private final QpackHuffmanEncoder huffmanEncoder; + private final QpackEncoderDynamicTable dynamicTable; + private int maxBlockedStreams; + private int blockedStreams; + private LongObjectHashMap> streamSectionTrackers; + + QpackEncoder() { + this(new QpackEncoderDynamicTable()); + } + + QpackEncoder(QpackEncoderDynamicTable dynamicTable) { + huffmanEncoder = new QpackHuffmanEncoder(); + this.dynamicTable = dynamicTable; + } + + /** + * Encode the header field into the header block. + * + * TODO: do we need to support sensitivity detector? + */ + void encodeHeaders(QpackAttributes qpackAttributes, ByteBuf out, ByteBufAllocator allocator, long streamId, + Http3Headers headers) { + final int base = dynamicTable.insertCount(); + // Allocate a new buffer as we have to go back and write a variable length base and required insert count + // later. + ByteBuf tmp = allocator.buffer(); + try { + int maxDynamicTblIdx = -1; + int requiredInsertCount = 0; + Indices dynamicTableIndices = null; + for (Map.Entry header : headers) { + CharSequence name = header.getKey(); + CharSequence value = header.getValue(); + int dynamicTblIdx = encodeHeader(qpackAttributes, tmp, base, name, value); + if (dynamicTblIdx >= 0) { + int req = dynamicTable.addReferenceToEntry(name, value, dynamicTblIdx); + if (dynamicTblIdx > maxDynamicTblIdx) { + maxDynamicTblIdx = dynamicTblIdx; + requiredInsertCount = req; + } + if (dynamicTableIndices == null) { + dynamicTableIndices = new Indices(); + } + dynamicTableIndices.add(dynamicTblIdx); + } + } + + // Track all the indices that we need to ack later. + if (dynamicTableIndices != null) { + assert streamSectionTrackers != null; + streamSectionTrackers.computeIfAbsent(streamId, __ -> new ArrayDeque<>()) + .add(dynamicTableIndices); + } + + // https://www.rfc-editor.org/rfc/rfc9204.html#name-encoded-field-section-prefi + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | Required Insert Count (8+) | + // +---+---------------------------+ + // | S | Delta Base (7+) | + // +---+---------------------------+ + encodePrefixedInteger(out, (byte) 0b0, 8, dynamicTable.encodedRequiredInsertCount(requiredInsertCount)); + if (base >= requiredInsertCount) { + encodePrefixedInteger(out, (byte) 0b0, 7, base - requiredInsertCount); + } else { + encodePrefixedInteger(out, (byte) 0b1000_0000, 7, requiredInsertCount - base - 1); + } + out.writeBytes(tmp); + } finally { + tmp.release(); + } + } + + void configureDynamicTable(QpackAttributes attributes, long maxTableCapacity, int blockedStreams) + throws QpackException { + if (maxTableCapacity > 0) { + assert attributes.encoderStreamAvailable(); + final QuicStreamChannel encoderStream = attributes.encoderStream(); + dynamicTable.maxTableCapacity(maxTableCapacity); + final ByteBuf tableCapacity = encoderStream.alloc().buffer(8); + // https://www.rfc-editor.org/rfc/rfc9204.html#name-set-dynamic-table-capacity + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Capacity (5+) | + // +---+---+---+-------------------+ + encodePrefixedInteger(tableCapacity, (byte) 0b0010_0000, 5, maxTableCapacity); + closeOnFailure(encoderStream.writeAndFlush(tableCapacity)); + + streamSectionTrackers = new LongObjectHashMap<>(); + maxBlockedStreams = blockedStreams; + } + } + + /** + * + * Section acknowledgment for the passed {@code streamId}. + * + * @param streamId For which the header fields section is acknowledged. + */ + void sectionAcknowledgment(long streamId) throws QpackException { + assert streamSectionTrackers != null; + final Queue tracker = streamSectionTrackers.get(streamId); + if (tracker == null) { + throw INVALID_SECTION_ACKNOWLEDGMENT; + } + + Indices dynamicTableIndices = tracker.poll(); + + if (tracker.isEmpty()) { + streamSectionTrackers.remove(streamId); + } + + if (dynamicTableIndices == null) { + throw INVALID_SECTION_ACKNOWLEDGMENT; + } + + dynamicTableIndices.forEach(dynamicTable::acknowledgeInsertCountOnAck); + } + + /** + * + * Stream cancellation for the passed {@code streamId}. + * + * @param streamId which is cancelled. + */ + void streamCancellation(long streamId) throws QpackException { + // If a configureDynamicTable(...) was called with a maxTableCapacity of 0 we will have not instanced + // streamSectionTrackers. The remote peer might still send a stream cancellation for a stream, while it + // is optional. See https://www.rfc-editor.org/rfc/rfc9204.html#section-2.2.2.2 + if (streamSectionTrackers == null) { + return; + } + final Queue tracker = streamSectionTrackers.remove(streamId); + if (tracker != null) { + for (;;) { + Indices dynamicTableIndices = tracker.poll(); + if (dynamicTableIndices == null) { + break; + } + dynamicTableIndices.forEach(dynamicTable::acknowledgeInsertCountOnCancellation); + } + } + } + + /** + * + * Insert count increment. + * + * @param increment for the known received count. + */ + void insertCountIncrement(int increment) throws QpackException { + dynamicTable.incrementKnownReceivedCount(increment); + } + + /** + * Encode the header field into the header block. + * @param qpackAttributes {@link QpackAttributes} for the channel. + * @param out {@link ByteBuf} to which encoded header field is to be written. + * @param base Base for the dynamic table index. + * @param name for the header field. + * @param value for the header field. + * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table, + * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE } otherwise. + */ + private int encodeHeader(QpackAttributes qpackAttributes, ByteBuf out, int base, CharSequence name, + CharSequence value) { + int index = QpackStaticTable.findFieldIndex(name, value); + if (index == QpackStaticTable.NOT_FOUND) { + if (qpackAttributes.dynamicTableDisabled()) { + encodeLiteral(out, name, value); + return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE; + } + return encodeWithDynamicTable(qpackAttributes, out, base, name, value); + } else if ((index & QpackStaticTable.MASK_NAME_REF) == QpackStaticTable.MASK_NAME_REF) { + int dynamicTblIdx = tryEncodeWithDynamicTable(qpackAttributes, out, base, name, value); + if (dynamicTblIdx >= 0) { + return dynamicTblIdx; + } + final int nameIdx = index ^ QpackStaticTable.MASK_NAME_REF; + dynamicTblIdx = tryAddToDynamicTable(qpackAttributes, true, nameIdx, name, value); + if (dynamicTblIdx >= 0) { + if (dynamicTblIdx >= base) { + encodePostBaseIndexed(out, base, dynamicTblIdx); + } else { + encodeIndexedDynamicTable(out, base, dynamicTblIdx); + } + return dynamicTblIdx; + } + encodeLiteralWithNameRefStaticTable(out, nameIdx, value); + } else { + encodeIndexedStaticTable(out, index); + } + return qpackAttributes.dynamicTableDisabled() ? DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE : + DYNAMIC_TABLE_ENCODE_NOT_DONE; + } + + /** + * Encode the header field using dynamic table, if possible. + * + * @param qpackAttributes {@link QpackAttributes} for the channel. + * @param out {@link ByteBuf} to which encoded header field is to be written. + * @param base Base for the dynamic table index. + * @param name for the header field. + * @param value for the header field. + * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table, + * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE } otherwise. + */ + private int encodeWithDynamicTable(QpackAttributes qpackAttributes, ByteBuf out, int base, CharSequence name, + CharSequence value) { + int idx = tryEncodeWithDynamicTable(qpackAttributes, out, base, name, value); + if (idx >= 0) { + return idx; + } + + if (idx == DYNAMIC_TABLE_ENCODE_NOT_DONE) { + idx = tryAddToDynamicTable(qpackAttributes, false, -1, name, value); + if (idx >= 0) { + if (idx >= base) { + encodePostBaseIndexed(out, base, idx); + } else { + encodeIndexedDynamicTable(out, base, idx); + } + return idx; + } + } + encodeLiteral(out, name, value); + return idx; + } + + /** + * Try to encode the header field using dynamic table, otherwise do not encode. + * + * @param qpackAttributes {@link QpackAttributes} for the channel. + * @param out {@link ByteBuf} to which encoded header field is to be written. + * @param base Base for the dynamic table index. + * @param name for the header field. + * @param value for the header field. + * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table. + * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE } if encoding was not done. {@link #DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE } + * if dynamic table encoding is not possible (size constraint) and hence should not be tried for this header. + */ + private int tryEncodeWithDynamicTable(QpackAttributes qpackAttributes, ByteBuf out, int base, CharSequence name, + CharSequence value) { + if (qpackAttributes.dynamicTableDisabled()) { + return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE; + } + assert qpackAttributes.encoderStreamAvailable(); + final QuicStreamChannel encoderStream = qpackAttributes.encoderStream(); + + int idx = dynamicTable.getEntryIndex(name, value); + if (idx == QpackEncoderDynamicTable.NOT_FOUND) { + return DYNAMIC_TABLE_ENCODE_NOT_DONE; + } + if (idx >= 0) { + if (dynamicTable.requiresDuplication(idx, sizeOf(name, value))) { + idx = dynamicTable.add(name, value, sizeOf(name, value)); + assert idx >= 0; + // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.3.4 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | Index (5+) | + // +---+---+---+-------------------+ + ByteBuf duplicate = encoderStream.alloc().buffer(8); + encodePrefixedInteger(duplicate, (byte) 0b0000_0000, 5, + dynamicTable.relativeIndexForEncoderInstructions(idx)); + closeOnFailure(encoderStream.writeAndFlush(duplicate)); + if (mayNotBlockStream()) { + // Add to the table but do not use the entry in the header block to avoid blocking. + return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE; + } + } + if (idx >= base) { + encodePostBaseIndexed(out, base, idx); + } else { + encodeIndexedDynamicTable(out, base, idx); + } + } else { // name match + idx = -(idx + 1); + int addIdx = tryAddToDynamicTable(qpackAttributes, false, + dynamicTable.relativeIndexForEncoderInstructions(idx), name, value); + if (addIdx < 0) { + return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE; + } + idx = addIdx; + + if (idx >= base) { + encodeLiteralWithPostBaseNameRef(out, base, idx, value); + } else { + encodeLiteralWithNameRefDynamicTable(out, base, idx, value); + } + } + return idx; + } + + /** + * Try adding the header field to the dynamic table. + * + * @param qpackAttributes {@link QpackAttributes} for the channel. + * @param staticTableNameRef if {@code nameIdx} is an index in the static table. + * @param nameIdx Index of the name if {@code > 0}. + * @param name for the header field. + * @param value for the header field. + * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table, + * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE} otherwise. + */ + private int tryAddToDynamicTable(QpackAttributes qpackAttributes, boolean staticTableNameRef, int nameIdx, + CharSequence name, CharSequence value) { + if (qpackAttributes.dynamicTableDisabled()) { + return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE; + } + assert qpackAttributes.encoderStreamAvailable(); + final QuicStreamChannel encoderStream = qpackAttributes.encoderStream(); + + int idx = dynamicTable.add(name, value, sizeOf(name, value)); + if (idx >= 0) { + ByteBuf insert = null; + try { + if (nameIdx >= 0) { + // 2 prefixed integers (name index and value length) each requires a maximum of 8 bytes + insert = encoderStream.alloc().buffer(value.length() + 16); + // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-name-reference + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Name Index (6+) | + // +---+---+-----------------------+ + encodePrefixedInteger(insert, (byte) (staticTableNameRef ? 0b1100_0000 : 0b1000_0000), 6, nameIdx); + } else { + // 2 prefixed integers (name and value length) each requires a maximum of 8 bytes + insert = encoderStream.alloc().buffer(name.length() + value.length() + 16); + // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-literal-name + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | H | Name Length (5+) | + // +---+---+---+-------------------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // TODO: Force H = 1 till we support sensitivity detector + encodeLengthPrefixedHuffmanEncodedLiteral(insert, (byte) 0b0110_0000, 5, name); + } + // 0 1 2 3 4 5 6 7 + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + encodeStringLiteral(insert, value); + } catch (Exception e) { + ReferenceCountUtil.release(insert); + return DYNAMIC_TABLE_ENCODE_NOT_DONE; + } + closeOnFailure(encoderStream.writeAndFlush(insert)); + if (mayNotBlockStream()) { + // Add to the table but do not use the entry in the header block to avoid blocking. + return DYNAMIC_TABLE_ENCODE_NOT_DONE; + } + blockedStreams++; + } + return idx; + } + + private void encodeIndexedStaticTable(ByteBuf out, int index) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---+-----------------------+ + encodePrefixedInteger(out, (byte) 0b1100_0000, 6, index); + } + + private void encodeIndexedDynamicTable(ByteBuf out, int base, int index) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---+-----------------------+ + encodePrefixedInteger(out, (byte) 0b1000_0000, 6, base - index - 1); + } + + private void encodePostBaseIndexed(ByteBuf out, int base, int index) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line-with-pos + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+---+---+---------------+ + encodePrefixedInteger(out, (byte) 0b0001_0000, 4, index - base); + } + + private void encodeLiteralWithNameRefStaticTable(ByteBuf out, int nameIndex, CharSequence value) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T |Name Index (4+)| + // +---+---+---+---+---------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // TODO: Force N = 0 till we support sensitivity detector + encodePrefixedInteger(out, (byte) 0b0101_0000, 4, nameIndex); + encodeStringLiteral(out, value); + } + + private void encodeLiteralWithNameRefDynamicTable(ByteBuf out, int base, int nameIndex, CharSequence value) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T |Name Index (4+)| + // +---+---+---+---+---------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // TODO: Force N = 0 till we support sensitivity detector + encodePrefixedInteger(out, (byte) 0b0101_0000, 4, base - nameIndex - 1); + encodeStringLiteral(out, value); + } + + private void encodeLiteralWithPostBaseNameRef(ByteBuf out, int base, int nameIndex, CharSequence value) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-pos + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | N |NameIdx(3+)| + // +---+---+---+---+---+-----------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // TODO: Force N = 0 till we support sensitivity detector + encodePrefixedInteger(out, (byte) 0b0000_0000, 4, nameIndex - base); + encodeStringLiteral(out, value); + } + + private void encodeLiteral(ByteBuf out, CharSequence name, CharSequence value) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-lit + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | N | H |NameLen(3+)| + // +---+---+---+---+---+-----------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // TODO: Force N = 0 & H = 1 till we support sensitivity detector + encodeLengthPrefixedHuffmanEncodedLiteral(out, (byte) 0b0010_1000, 3, name); + encodeStringLiteral(out, value); + } + + /** + * Encode string literal according to Section 5.2. + * Section 5.2. + */ + private void encodeStringLiteral(ByteBuf out, CharSequence value) { + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | H | String Length (7+) | + // +---+---------------------------+ + // | String Data (Length octets) | + // +-------------------------------+ + // TODO: Force H = 1 till we support sensitivity detector + encodeLengthPrefixedHuffmanEncodedLiteral(out, (byte) 0b1000_0000, 7, value); + } + + /** + * Encode a string literal. + */ + private void encodeLengthPrefixedHuffmanEncodedLiteral(ByteBuf out, byte mask, int prefix, CharSequence value) { + int huffmanLength = huffmanEncoder.getEncodedLength(value); + encodePrefixedInteger(out, mask, prefix, huffmanLength); + huffmanEncoder.encode(out, value); + } + + private boolean mayNotBlockStream() { + return blockedStreams >= maxBlockedStreams - 1; + } + + private static final class Indices { + private int idx; + // Let's just assume 4 indices for now that we will store here as max. + private int[] array = new int[4]; + + void add(int index) { + if (idx == array.length) { + // Double it if needed. + array = Arrays.copyOf(array, array.length << 1); + } + array[idx++] = index; + } + + void forEach(IndexConsumer consumer) throws QpackException { + for (int i = 0; i < idx; i++) { + consumer.accept(array[i]); + } + } + + @FunctionalInterface + interface IndexConsumer { + void accept(int idx) throws QpackException; + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoderDynamicTable.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoderDynamicTable.java new file mode 100644 index 0000000..29e3dbb --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoderDynamicTable.java @@ -0,0 +1,489 @@ +/* + * 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.handler.codec.http3; + +import io.netty.util.AsciiString; + +import static io.netty.handler.codec.http3.QpackHeaderField.ENTRY_OVERHEAD; +import static io.netty.handler.codec.http3.QpackUtil.MAX_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http3.QpackUtil.MIN_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http3.QpackUtil.equalsVariableTime; +import static io.netty.util.AsciiString.EMPTY_STRING; +import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.toIntExact; + +final class QpackEncoderDynamicTable { + private static final QpackException INVALID_KNOW_RECEIVED_COUNT_INCREMENT = + QpackException.newStatic(QpackDecoder.class, "incrementKnownReceivedCount(...)", + "QPACK - invalid known received count increment."); + private static final QpackException INVALID_REQUIRED_INSERT_COUNT_INCREMENT = + QpackException.newStatic(QpackDecoder.class, "acknowledgeInsertCount(...)", + "QPACK - invalid required insert count acknowledgment."); + private static final QpackException INVALID_TABLE_CAPACITY = + QpackException.newStatic(QpackDecoder.class, "validateCapacity(...)", + "QPACK - dynamic table capacity is invalid."); + private static final QpackException CAPACITY_ALREADY_SET = + QpackException.newStatic(QpackDecoder.class, "maxTableCapacity(...)", + "QPACK - dynamic table capacity is already set."); + /** + * Special return value of {@link #getEntryIndex(CharSequence, CharSequence)} when the entry is not found. + */ + public static final int NOT_FOUND = Integer.MIN_VALUE; + + /** + * A hashmap of header entries. + */ + private final HeaderEntry[] fields; + + /** + * Percentage of capacity that we expect to be free after eviction of old entries. + */ + private final int expectedFreeCapacityPercentage; + + /** + * Hash mask for all entries in the hashmap. + */ + private final byte hashMask; + + /** + * Current size of the table. + */ + private long size; + + /** + * + * Maximum capacity of the table. This is set once based on the + * {@link Http3SettingsFrame#HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY} received by the remote peer. + */ + private long maxTableCapacity = -1; + + /* + * The below indexes follow the suggested heuristics in Section 2.1.1.1 Avoiding Prohibited insertions + * https://www.rfc-editor.org/rfc/rfc9204.html#name-avoiding-prohibited-inserti + * + * Tail Drain Head + * | | | + * v v v + * +--------+---------------------------------+----------+ + * | Unused | Referenceable | Draining | + * | Space | Entries | Entries | + * +--------+---------------------------------+----------+ + * ^ ^ ^ + * | | | + * Insertion Index Draining Index Dropping Index + */ + + /** + * Head of the entries, such that {@link HeaderEntry#index} is the {@code droppingIndex}. + */ + private final HeaderEntry head; + + /** + * Pointer before which entries are marked for eviction post {@link #incrementKnownReceivedCount(int)}. + * {@link HeaderEntry#index} is the {@code drainingIndex}. + */ + private HeaderEntry drain; + + /** + * Pointer to the entry representing the + * known received count. + */ + private HeaderEntry knownReceived; + + /** + * Tail of the entries, such that {@link HeaderEntry#index} is the {@code insertionIndex}. + */ + private HeaderEntry tail; + + QpackEncoderDynamicTable() { + this(16, 10); + } + + QpackEncoderDynamicTable(int arraySizeHint, int expectedFreeCapacityPercentage) { + // Enforce a bound of [2, 128] because hashMask is a byte. The max possible value of hashMask is one less + // than the length of this array, and we want the mask to be > 0. + fields = new HeaderEntry[findNextPositivePowerOfTwo(max(2, min(arraySizeHint, 128)))]; + hashMask = (byte) (fields.length - 1); + // Start with index -1 so the first added header will have the index of 0. + // See https://www.rfc-editor.org/rfc/rfc9204.html#name-absolute-indexing + head = new HeaderEntry(-1, EMPTY_STRING, EMPTY_STRING, -1, null); + this.expectedFreeCapacityPercentage = expectedFreeCapacityPercentage; + resetIndicesToHead(); + } + + /** + * Add a name - value pair to the dynamic table and returns the index. + * + * @param name the name. + * @param value the value. + * @param headerSize the size of the header. + * @return the absolute index or {@code -1) if it could not be added. + */ + int add(CharSequence name, CharSequence value, long headerSize) { + if (maxTableCapacity - size < headerSize) { + return -1; + } + + if (tail.index == Integer.MAX_VALUE) { + // Wait for all entries to evict before we restart indexing from zero + evictUnreferencedEntries(); + return -1; + } + int h = AsciiString.hashCode(name); + int i = index(h); + HeaderEntry old = fields[i]; + HeaderEntry e = new HeaderEntry(h, name, value, tail.index + 1, old); + fields[i] = e; + e.addNextTo(tail); + tail = e; + size += headerSize; + + ensureFreeCapacity(); + return e.index; + } + + /** + * Callback when a header block which had a {@link #insertCount()}} greater than {@code 0} is + * acknowledged + * by the decoder. + * + * @param entryIndex For the entry corresponding to the {@link #insertCount()}. + * @throws QpackException If the count is invalid. + */ + void acknowledgeInsertCountOnAck(int entryIndex) throws QpackException { + acknowledgeInsertCount(entryIndex, true); + } + + /** + * Callback when a header block which had a {@link #insertCount()}} greater than {@code 0} is still not processed + * and the stream is cancelled + * by the decoder. + * + * @param entryIndex For the entry corresponding to the {@link #insertCount()}. + * @throws QpackException If the count is invalid. + */ + void acknowledgeInsertCountOnCancellation(int entryIndex) throws QpackException { + acknowledgeInsertCount(entryIndex, false); + } + + private void acknowledgeInsertCount(int entryIndex, boolean updateKnownReceived) throws QpackException { + if (entryIndex < 0) { + throw INVALID_REQUIRED_INSERT_COUNT_INCREMENT; + } + for (HeaderEntry e = head.next; e != null; e = e.next) { + if (e.index == entryIndex) { + assert e.refCount > 0; + e.refCount--; + if (updateKnownReceived && e.index > knownReceived.index) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-known-received-count + // If the Required Insert Count of the acknowledged field section is greater than the current Known + // Received Count, Known Received Count is updated to that Required Insert Count value. + knownReceived = e; + } + evictUnreferencedEntries(); + return; + } + } + // We have reached the end of the linked list so the index was invalid and hence the connection should + // be closed. + // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.4 + throw INVALID_REQUIRED_INSERT_COUNT_INCREMENT; + } + + /** + * Callback when a decoder increments its + * insert count. + * + * @param knownReceivedCountIncr Increment count. + * @throws QpackException If the increment count is invalid. + */ + void incrementKnownReceivedCount(int knownReceivedCountIncr) throws QpackException { + if (knownReceivedCountIncr <= 0) { + throw INVALID_KNOW_RECEIVED_COUNT_INCREMENT; + } + while (knownReceived.next != null && knownReceivedCountIncr > 0) { + knownReceived = knownReceived.next; + knownReceivedCountIncr--; + } + if (knownReceivedCountIncr == 0) { + evictUnreferencedEntries(); + return; + } + // We have reached the end of the linked list so the index was invalid and hence the connection should be + // closed. + // https://www.rfc-editor.org/rfc/rfc9204.html#name-decoder-instructions + throw INVALID_KNOW_RECEIVED_COUNT_INCREMENT; + } + + /** + * Returns the number of entries inserted to this dynamic table. + * + * @return number the added entries. + */ + int insertCount() { + return tail.index + 1; + } + + /** + * + * Encodes the required insert count. + * @param reqInsertCount the required insert count. + * @return the encoded count. + */ + int encodedRequiredInsertCount(int reqInsertCount) { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count + // if ReqInsertCount == 0: + // EncInsertCount = 0 + // else: + // EncInsertCount = (ReqInsertCount mod (2 * MaxEntries)) + 1 + // + return reqInsertCount == 0 ? 0 : reqInsertCount % toIntExact(2 * QpackUtil.maxEntries(maxTableCapacity)) + 1; + } + + // Visible for tests + int encodedKnownReceivedCount() { + // https://www.rfc-editor.org/rfc/rfc9204.html#name-known-received-count + return encodedRequiredInsertCount(knownReceived.index + 1); + } + + /** + * Set the maximum capacity of the dynamic table. This can only be set once. + * @param capacity the capacity + * @throws QpackException if capacity was set before. + */ + void maxTableCapacity(long capacity) throws QpackException { + validateCapacity(capacity); + if (this.maxTableCapacity >= 0) { + throw CAPACITY_ALREADY_SET; + } + this.maxTableCapacity = capacity; + } + + /** + * Transforms the passed {@code entryIndex} as a relative index for + * encoder instructions. + * + * @param entryIndex to transform. + * @return Relative index for the passed {@code entryIndex}. + */ + int relativeIndexForEncoderInstructions(int entryIndex) { + assert entryIndex >= 0; + assert entryIndex <= tail.index; + return tail.index - entryIndex; + } + + /** + * Finds an entry with the passed {@code name} and {@code value} in this dynamic table. + * + * @param name of the entry to find. + * @param value of the entry to find. + * @return {@link #NOT_FOUND} if the entry does not exist. If an entry with matching {@code name} and {@code value} + * exists, then the index is returned. If an entry with only matching name exists then {@code -index-1} is + * returned. + */ + int getEntryIndex(CharSequence name, CharSequence value) { + if (tail != head && name != null && value != null) { + int h = AsciiString.hashCode(name); + int i = index(h); + HeaderEntry firstNameMatch = null; + HeaderEntry entry = null; + for (HeaderEntry e = fields[i]; e != null; e = e.nextSibling) { + if (e.hash == h && equalsVariableTime(value, e.value)) { + if (equalsVariableTime(name, e.name)) { + entry = e; + break; + } + } else if (firstNameMatch == null && equalsVariableTime(name, e.name)) { + firstNameMatch = e; + } + } + if (entry != null) { + return entry.index; + } + if (firstNameMatch != null) { + return -firstNameMatch.index - 1; + } + } + return NOT_FOUND; + } + + /** + * Adds a reference to an entry at the passed {@code idx}. + * + * @param name of the entry for lookups, not verified for the entry at the pased {@code idx} + * @param value of the entry for lookups, not verified for the entry at the pased {@code idx} + * @param idx of the entry. + * @return Required + * insert count if the passed entry has to be referenced in a header block. + */ + int addReferenceToEntry(CharSequence name, CharSequence value, int idx) { + if (tail != head && name != null && value != null) { + int h = AsciiString.hashCode(name); + int i = index(h); + for (HeaderEntry e = fields[i]; e != null; e = e.nextSibling) { + if (e.hash == h && idx == e.index) { + e.refCount++; + return e.index + 1; + } + } + } + throw new IllegalArgumentException("Index " + idx + " not found"); + } + + boolean requiresDuplication(int idx, long size) { + assert head != tail; + + if (this.size + size > maxTableCapacity || head == drain) { + return false; + } + return idx >= head.next.index && idx <= drain.index; + } + + private void evictUnreferencedEntries() { + if (head == knownReceived || head == drain) { + return; + } + + while (head.next != null && head.next != knownReceived.next && head.next != drain.next) { + if (!removeIfUnreferenced()) { + return; + } + } + } + + private boolean removeIfUnreferenced() { + final HeaderEntry toRemove = head.next; + if (toRemove.refCount != 0) { + return false; + } + size -= toRemove.size(); + + // Remove from the hash map + final int i = index(toRemove.hash); + HeaderEntry e = fields[i]; + HeaderEntry prev = null; + while (e != null && e != toRemove) { + prev = e; + e = e.nextSibling; + } + if (e == toRemove) { + if (prev == null) { + fields[i] = e.nextSibling; + } else { + prev.nextSibling = e.nextSibling; + } + } + + // Remove from the linked list + toRemove.remove(head); + if (toRemove == tail) { + resetIndicesToHead(); + } + if (toRemove == drain) { + drain = head; + } + if (toRemove == knownReceived) { + knownReceived = head; + } + return true; + } + + private void resetIndicesToHead() { + tail = head; + drain = head; + knownReceived = head; + } + + private void ensureFreeCapacity() { + long maxDesiredSize = max(ENTRY_OVERHEAD, ((100 - expectedFreeCapacityPercentage) * maxTableCapacity) / 100); + long cSize = size; + HeaderEntry nDrain; + for (nDrain = head; nDrain.next != null && cSize > maxDesiredSize; nDrain = nDrain.next) { + cSize -= nDrain.next.size(); + } + if (cSize != size) { + drain = nDrain; + evictUnreferencedEntries(); + } + } + + private int index(int h) { + return h & hashMask; + } + + private static void validateCapacity(long capacity) throws QpackException { + if (capacity < MIN_HEADER_TABLE_SIZE || capacity > MAX_HEADER_TABLE_SIZE) { + throw INVALID_TABLE_CAPACITY; + } + } + + /** + * An entry for the {@link #fields} HashMap. This entry provides insertion order iteration using {@link #next}. + */ + private static final class HeaderEntry extends QpackHeaderField { + /** + * Pointer to the next entry in insertion order with a different {@link #hash} than this entry. + */ + HeaderEntry next; + + /** + * Pointer to the next entry in insertion order with the same {@link #hash} as this entry, a.k.a hash collisions + */ + HeaderEntry nextSibling; + + /** + * Number of header blocks that refer to this entry as the value for its + * required insert count + */ + int refCount; + + /** + * Hashcode for this entry. + */ + final int hash; + + /** + * Insertion index for this entry. + */ + final int index; + + HeaderEntry(int hash, CharSequence name, CharSequence value, int index, HeaderEntry nextSibling) { + super(name, value); + this.index = index; + this.hash = hash; + this.nextSibling = nextSibling; + } + + void remove(HeaderEntry prev) { + assert prev != this; + prev.next = next; + next = null; // null references to prevent nepotism in generational GC. + nextSibling = null; + } + + void addNextTo(HeaderEntry prev) { + assert prev != this; + this.next = prev.next; + prev.next = this; + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoderHandler.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoderHandler.java new file mode 100644 index 0000000..291e214 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackEncoderHandler.java @@ -0,0 +1,245 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.AsciiString; + +import java.util.List; + +import static io.netty.handler.codec.http3.Http3CodecUtils.connectionError; +import static io.netty.handler.codec.http3.Http3ErrorCode.QPACK_ENCODER_STREAM_ERROR; +import static io.netty.handler.codec.http3.QpackUtil.MAX_UNSIGNED_INT; +import static io.netty.handler.codec.http3.QpackUtil.decodePrefixedIntegerAsInt; +import static io.netty.util.internal.ObjectUtil.checkInRange; + +final class QpackEncoderHandler extends ByteToMessageDecoder { + + private final QpackHuffmanDecoder huffmanDecoder; + private final QpackDecoder qpackDecoder; + private boolean discard; + + QpackEncoderHandler(Long maxTableCapacity, QpackDecoder qpackDecoder) { + checkInRange(maxTableCapacity == null ? 0 : maxTableCapacity, 0, MAX_UNSIGNED_INT, "maxTableCapacity"); + huffmanDecoder = new QpackHuffmanDecoder(); + this.qpackDecoder = qpackDecoder; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List __) throws Exception { + if (!in.isReadable()) { + return; + } + if (discard) { + in.skipBytes(in.readableBytes()); + return; + } + + byte b = in.getByte(in.readerIndex()); + + // 4.3.1. Set Dynamic Table Capacity + // + // 0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 0 | 1 | Capacity (5+) | + //+---+---+---+-------------------+ + if ((b & 0b1110_0000) == 0b0010_0000) { + // new capacity + long capacity = QpackUtil.decodePrefixedInteger(in, 5); + if (capacity < 0) { + // Not enough readable bytes + return; + } + + try { + qpackDecoder.setDynamicTableCapacity(capacity); + } catch (QpackException e) { + handleDecodeFailure(ctx, e, "setDynamicTableCapacity failed."); + } + return; + } + + final QpackAttributes qpackAttributes = Http3.getQpackAttributes(ctx.channel().parent()); + assert qpackAttributes != null; + if (!qpackAttributes.dynamicTableDisabled() && !qpackAttributes.decoderStreamAvailable()) { + // We need the decoder stream to update the decoder with these instructions. + return; + } + final QuicStreamChannel decoderStream = qpackAttributes.decoderStream(); + + // 4.3.2. Insert With Name Reference + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Name Index (6+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + if ((b & 0b1000_0000) == 0b1000_0000) { + int readerIndex = in.readerIndex(); + // T == 1 implies static table index. + // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-name-reference + final boolean isStaticTableIndex = QpackUtil.firstByteEquals(in, (byte) 0b1100_0000); + final int nameIdx = decodePrefixedIntegerAsInt(in, 6); + if (nameIdx < 0) { + // Not enough readable bytes + return; + } + + CharSequence value = decodeLiteralValue(in); + if (value == null) { + // Reset readerIndex + in.readerIndex(readerIndex); + // Not enough readable bytes + return; + } + try { + qpackDecoder.insertWithNameReference(decoderStream, isStaticTableIndex, nameIdx, + value); + } catch (QpackException e) { + handleDecodeFailure(ctx, e, "insertWithNameReference failed."); + } + return; + } + // 4.3.3. Insert With Literal Name + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | H | Name Length (5+) | + // +---+---+---+-------------------+ + // | Name String (Length bytes) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + if ((b & 0b1100_0000) == 0b0100_0000) { + int readerIndex = in.readerIndex(); + final boolean nameHuffEncoded = QpackUtil.firstByteEquals(in, (byte) 0b0110_0000); + int nameLength = decodePrefixedIntegerAsInt(in, 5); + if (nameLength < 0) { + // Reset readerIndex + in.readerIndex(readerIndex); + // Not enough readable bytes + return; + } + if (in.readableBytes() < nameLength) { + // Reset readerIndex + in.readerIndex(readerIndex); + // Not enough readable bytes + return; + } + + CharSequence name = decodeStringLiteral(in, nameHuffEncoded, nameLength); + CharSequence value = decodeLiteralValue(in); + if (value == null) { + // Reset readerIndex + in.readerIndex(readerIndex); + // Not enough readable bytes + return; + } + try { + qpackDecoder.insertLiteral(decoderStream, name, value); + } catch (QpackException e) { + handleDecodeFailure(ctx, e, "insertLiteral failed."); + } + return; + } + // 4.3.4. Duplicate + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | Index (5+) | + // +---+---+---+-------------------+ + if ((b & 0b1110_0000) == 0b0000_0000) { + int readerIndex = in.readerIndex(); + int index = decodePrefixedIntegerAsInt(in, 5); + if (index < 0) { + // Reset readerIndex + in.readerIndex(readerIndex); + // Not enough readable bytes + return; + } + try { + qpackDecoder.duplicate(decoderStream, index); + } catch (QpackException e) { + handleDecodeFailure(ctx, e, "duplicate failed."); + } + return; + } + + discard = true; + Http3CodecUtils.connectionError(ctx, Http3ErrorCode.QPACK_ENCODER_STREAM_ERROR, + "Unknown encoder instruction '" + b + "'.", false); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.fireChannelReadComplete(); + + // QPACK streams should always be processed, no matter what the user is doing in terms of configuration + // and AUTO_READ. + Http3CodecUtils.readIfNoAutoRead(ctx); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof ChannelInputShutdownEvent) { + // See https://www.rfc-editor.org/rfc/rfc9204.html#name-encoder-and-decoder-streams + Http3CodecUtils.criticalStreamClosed(ctx); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + // See https://www.rfc-editor.org/rfc/rfc9204.html#name-encoder-and-decoder-streams + Http3CodecUtils.criticalStreamClosed(ctx); + ctx.fireChannelInactive(); + } + + private void handleDecodeFailure(ChannelHandlerContext ctx, QpackException cause, String message) { + discard = true; + connectionError(ctx, new Http3Exception(QPACK_ENCODER_STREAM_ERROR, message, cause), true); + } + + private CharSequence decodeLiteralValue(ByteBuf in) throws QpackException { + final boolean valueHuffEncoded = QpackUtil.firstByteEquals(in, (byte) 0b1000_0000); + int valueLength = decodePrefixedIntegerAsInt(in, 7); + if (valueLength < 0 || in.readableBytes() < valueLength) { + // Not enough readable bytes + return null; + } + + return decodeStringLiteral(in, valueHuffEncoded, valueLength); + } + + private CharSequence decodeStringLiteral(ByteBuf in, boolean huffmanEncoded, int length) + throws QpackException { + if (huffmanEncoded) { + return huffmanDecoder.decode(in, length); + } + byte[] buf = new byte[length]; + in.readBytes(buf); + return new AsciiString(buf, false); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackException.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackException.java new file mode 100644 index 0000000..3f118aa --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.util.internal.ThrowableUtil; + +/** + * Exception thrown if an error happens during QPACK processing. + */ +public final class QpackException extends Exception { + + private QpackException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + static QpackException newStatic(Class clazz, String method, String message) { + return ThrowableUtil.unknownStackTrace(new QpackException(message, null, false, false), clazz, method); + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHeaderField.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHeaderField.java new file mode 100644 index 0000000..f2ab262 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHeaderField.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +class QpackHeaderField { + + /** + * + * Section 3.2.1 Dynamic Table Size. + * The size of an entry is the sum of its name's length in bytes, its + * value's length in bytes, and 32. + */ + static final int ENTRY_OVERHEAD = 32; + + static long sizeOf(CharSequence name, CharSequence value) { + return name.length() + value.length() + ENTRY_OVERHEAD; + } + + final CharSequence name; + final CharSequence value; + + // This constructor can only be used if name and value are ISO-8859-1 encoded. + QpackHeaderField(CharSequence name, CharSequence value) { + this.name = checkNotNull(name, "name"); + this.value = checkNotNull(value, "value"); + } + + long size() { + return sizeOf(name, value); + } + + @Override + public String toString() { + return name + ": " + value; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHuffmanDecoder.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHuffmanDecoder.java new file mode 100644 index 0000000..649f743 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHuffmanDecoder.java @@ -0,0 +1,4717 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.util.AsciiString; +import io.netty.util.ByteProcessor; + +final class QpackHuffmanDecoder implements ByteProcessor { + + /* Scroll to the bottom! */ + + private static final byte HUFFMAN_COMPLETE = 1; + private static final byte HUFFMAN_EMIT_SYMBOL = 1 << 1; + private static final byte HUFFMAN_FAIL = 1 << 2; + + private static final int HUFFMAN_COMPLETE_SHIFT = HUFFMAN_COMPLETE << 8; + private static final int HUFFMAN_EMIT_SYMBOL_SHIFT = HUFFMAN_EMIT_SYMBOL << 8; + private static final int HUFFMAN_FAIL_SHIFT = HUFFMAN_FAIL << 8; + + /** + * A table of byte tuples (state, flags, output). They are packed together as: + *

+ * state<<16 + flags<<8 + output + */ + private static final int[] HUFFS = new int[] { + // Node 0 (Root Node, never emits symbols.) + 4 << 16, + 5 << 16, + 7 << 16, + 8 << 16, + 11 << 16, + 12 << 16, + 16 << 16, + 19 << 16, + 25 << 16, + 28 << 16, + 32 << 16, + 35 << 16, + 42 << 16, + 49 << 16, + 57 << 16, + (64 << 16) + (HUFFMAN_COMPLETE << 8), + + // Node 1 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, + 13 << 16, + 14 << 16, + 17 << 16, + 18 << 16, + 20 << 16, + 21 << 16, + + // Node 2 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, + + // Node 3 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, + + // Node 4 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49, + + // Node 5 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97, + + // Node 6 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, + + // Node 7 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101, + + // Node 8 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111, + + // Node 9 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, + + // Node 10 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, + + // Node 11 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116, + + // Node 12 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, + + // Node 13 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37, + + // Node 14 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46, + + // Node 15 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, + + // Node 16 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + + // Node 17 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51, + + // Node 18 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53, + + // Node 19 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, + + // Node 20 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55, + + // Node 21 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57, + + // Node 22 + 26 << 16, + 27 << 16, + 29 << 16, + 30 << 16, + 33 << 16, + 34 << 16, + 36 << 16, + 37 << 16, + 43 << 16, + 46 << 16, + 50 << 16, + 53 << 16, + 58 << 16, + 61 << 16, + 65 << 16, + (68 << 16) + (HUFFMAN_COMPLETE << 8), + + // Node 23 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + 38 << 16, + 39 << 16, + + // Node 24 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + + // Node 25 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + + // Node 26 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65, + + // Node 27 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98, + + // Node 28 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + + // Node 29 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102, + + // Node 30 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104, + + // Node 31 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 32 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + + // Node 33 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109, + + // Node 34 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112, + + // Node 35 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 36 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117, + + // Node 37 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 38 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66, + + // Node 39 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68, + + // Node 40 + 44 << 16, + 45 << 16, + 47 << 16, + 48 << 16, + 51 << 16, + 52 << 16, + 54 << 16, + 55 << 16, + 59 << 16, + 60 << 16, + 62 << 16, + 63 << 16, + 66 << 16, + 67 << 16, + 69 << 16, + (72 << 16) + (HUFFMAN_COMPLETE << 8), + + // Node 41 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 42 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + + // Node 43 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + + // Node 44 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70, + + // Node 45 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72, + + // Node 46 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + + // Node 47 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74, + + // Node 48 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76, + + // Node 49 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 50 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + + // Node 51 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78, + + // Node 52 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80, + + // Node 53 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 54 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82, + + // Node 55 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84, + + // Node 56 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + 70 << 16, + 71 << 16, + 73 << 16, + (74 << 16) + (HUFFMAN_COMPLETE << 8), + + // Node 57 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + + // Node 58 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + + // Node 59 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86, + + // Node 60 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89, + + // Node 61 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + + // Node 62 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107, + + // Node 63 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118, + + // Node 64 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + 75 << 16, + 78 << 16, + + // Node 65 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + + // Node 66 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120, + + // Node 67 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122, + + // Node 68 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + 76 << 16, + 77 << 16, + 79 << 16, + 81 << 16, + + // Node 69 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + + // Node 70 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42, + + // Node 71 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59, + + // Node 72 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + 80 << 16, + 82 << 16, + 84 << 16, + + // Node 73 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90, + + // Node 74 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + 83 << 16, + 85 << 16, + 88 << 16, + + // Node 75 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + + // Node 76 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34, + + // Node 77 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41, + + // Node 78 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + 86 << 16, + 87 << 16, + 89 << 16, + 90 << 16, + + // Node 79 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + + // Node 80 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43, + + // Node 81 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + (HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + 91 << 16, + 92 << 16, + + // Node 82 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + + // Node 83 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62, + + // Node 84 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8), + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + 93 << 16, + 94 << 16, + + // Node 85 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8), + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + + // Node 86 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8), + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8), + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36, + + // Node 87 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91, + + // Node 88 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + 95 << 16, + + // Node 89 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126, + + // Node 90 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + 96 << 16, + 110 << 16, + + // Node 91 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125, + + // Node 92 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + 97 << 16, + 101 << 16, + 111 << 16, + 133 << 16, + + // Node 93 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96, + + // Node 94 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123, + 98 << 16, + 99 << 16, + 102 << 16, + 105 << 16, + 112 << 16, + 119 << 16, + 134 << 16, + 153 << 16, + + // Node 95 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + 100 << 16, + 103 << 16, + 104 << 16, + 106 << 16, + 107 << 16, + 113 << 16, + 116 << 16, + 120 << 16, + 126 << 16, + 135 << 16, + 142 << 16, + 154 << 16, + 169 << 16, + + // Node 96 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + 108 << 16, + 109 << 16, + + // Node 97 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + + // Node 98 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195, + + // Node 99 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + + // Node 100 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130, + + // Node 101 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 102 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + + // Node 103 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162, + + // Node 104 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194, + + // Node 105 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 106 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226, + + // Node 107 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 108 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161, + + // Node 109 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172, + + // Node 110 + 114 << 16, + 115 << 16, + 117 << 16, + 118 << 16, + 121 << 16, + 123 << 16, + 127 << 16, + 130 << 16, + 136 << 16, + 139 << 16, + 143 << 16, + 146 << 16, + 155 << 16, + 162 << 16, + 170 << 16, + 180 << 16, + + // Node 111 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + 122 << 16, + 124 << 16, + 125 << 16, + 128 << 16, + 129 << 16, + 131 << 16, + 132 << 16, + + // Node 112 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + + // Node 113 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + + // Node 114 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177, + + // Node 115 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209, + + // Node 116 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + + // Node 117 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217, + + // Node 118 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229, + + // Node 119 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 120 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + + // Node 121 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + + // Node 122 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132, + + // Node 123 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + + // Node 124 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134, + + // Node 125 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146, + + // Node 126 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 127 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + + // Node 128 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156, + + // Node 129 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163, + + // Node 130 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 131 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169, + + // Node 132 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173, + + // Node 133 + 137 << 16, + 138 << 16, + 140 << 16, + 141 << 16, + 144 << 16, + 145 << 16, + 147 << 16, + 150 << 16, + 156 << 16, + 159 << 16, + 163 << 16, + 166 << 16, + 171 << 16, + 174 << 16, + 181 << 16, + 190 << 16, + + // Node 134 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + 148 << 16, + 149 << 16, + 151 << 16, + 152 << 16, + + // Node 135 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + + // Node 136 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + + // Node 137 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181, + + // Node 138 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186, + + // Node 139 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + + // Node 140 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189, + + // Node 141 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196, + + // Node 142 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 143 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + + // Node 144 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228, + + // Node 145 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233, + + // Node 146 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 147 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + + // Node 148 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135, + + // Node 149 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138, + + // Node 150 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 151 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140, + + // Node 152 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143, + + // Node 153 + 157 << 16, + 158 << 16, + 160 << 16, + 161 << 16, + 164 << 16, + 165 << 16, + 167 << 16, + 168 << 16, + 172 << 16, + 173 << 16, + 175 << 16, + 177 << 16, + 182 << 16, + 185 << 16, + 191 << 16, + 207 << 16, + + // Node 154 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 155 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + + // Node 156 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + + // Node 157 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149, + + // Node 158 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151, + + // Node 159 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + + // Node 160 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155, + + // Node 161 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158, + + // Node 162 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 163 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + + // Node 164 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166, + + // Node 165 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174, + + // Node 166 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 167 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180, + + // Node 168 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183, + + // Node 169 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + 176 << 16, + 178 << 16, + 179 << 16, + 183 << 16, + 184 << 16, + 186 << 16, + 187 << 16, + 192 << 16, + 199 << 16, + 208 << 16, + 223 << 16, + + // Node 170 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 171 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + + // Node 172 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191, + + // Node 173 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231, + + // Node 174 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 175 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + + // Node 176 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142, + + // Node 177 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 178 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145, + + // Node 179 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159, + + // Node 180 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + 188 << 16, + 189 << 16, + 193 << 16, + 196 << 16, + 200 << 16, + 203 << 16, + 209 << 16, + 216 << 16, + 224 << 16, + 238 << 16, + + // Node 181 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 182 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + + // Node 183 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206, + + // Node 184 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225, + + // Node 185 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 186 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237, + + // Node 187 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 188 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207, + + // Node 189 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235, + + // Node 190 + 194 << 16, + 195 << 16, + 197 << 16, + 198 << 16, + 201 << 16, + 202 << 16, + 204 << 16, + 205 << 16, + 210 << 16, + 213 << 16, + 217 << 16, + 220 << 16, + 225 << 16, + 231 << 16, + 239 << 16, + 246 << 16, + + // Node 191 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + 206 << 16, + + // Node 192 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + + // Node 193 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + + // Node 194 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193, + + // Node 195 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201, + + // Node 196 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + + // Node 197 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205, + + // Node 198 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213, + + // Node 199 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 200 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + + // Node 201 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219, + + // Node 202 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240, + + // Node 203 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 204 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243, + + // Node 205 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 206 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204, + + // Node 207 + 211 << 16, + 212 << 16, + 214 << 16, + 215 << 16, + 218 << 16, + 219 << 16, + 221 << 16, + 222 << 16, + 226 << 16, + 228 << 16, + 232 << 16, + 235 << 16, + 240 << 16, + 243 << 16, + 247 << 16, + 250 << 16, + + // Node 208 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 209 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + + // Node 210 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + + // Node 211 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212, + + // Node 212 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221, + + // Node 213 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + + // Node 214 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223, + + // Node 215 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244, + + // Node 216 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 217 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + + // Node 218 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246, + + // Node 219 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248, + + // Node 220 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 221 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251, + + // Node 222 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253, + + // Node 223 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + 227 << 16, + 229 << 16, + 230 << 16, + 233 << 16, + 234 << 16, + 236 << 16, + 237 << 16, + 241 << 16, + 242 << 16, + 244 << 16, + 245 << 16, + 248 << 16, + 249 << 16, + 251 << 16, + 252 << 16, + + // Node 224 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 225 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + + // Node 226 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + + // Node 227 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3, + + // Node 228 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + + // Node 229 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5, + + // Node 230 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7, + + // Node 231 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 232 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + + // Node 233 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11, + + // Node 234 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14, + + // Node 235 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 236 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16, + + // Node 237 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18, + + // Node 238 + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + 253 << 16, + + // Node 239 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + + // Node 240 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + + // Node 241 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20, + + // Node 242 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23, + + // Node 243 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + + // Node 244 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25, + + // Node 245 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27, + + // Node 246 + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + 254 << 16, + 255 << 16, + + // Node 247 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + + // Node 248 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29, + + // Node 249 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31, + + // Node 250 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + HUFFMAN_FAIL << 8, + + // Node 251 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220, + + // Node 252 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + + // Node 253 + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + + // Node 254 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10, + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13, + + // Node 255 + (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22, + (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + HUFFMAN_FAIL << 8, + }; + + private static final QpackException BAD_ENCODING = QpackException.newStatic(QpackHuffmanDecoder.class, + "decode(...)", "QPACK - Bad Encoding"); + + private byte[] dest; + private int k; + private int state; + + QpackHuffmanDecoder() { } + + /** + * Decompresses the given Huffman coded string literal. + * + * @param buf the string literal to be decoded + * @return the output stream for the compressed data + * @throws QpackException EOS Decoded + */ + public AsciiString decode(ByteBuf buf, int length) throws QpackException { + if (length == 0) { + return AsciiString.EMPTY_STRING; + } + dest = new byte[length * 8 / 5]; + try { + int readerIndex = buf.readerIndex(); + // Using ByteProcessor to reduce bounds-checking and reference-count checking during byte-by-byte + // processing of the ByteBuf. + int endIndex = buf.forEachByte(readerIndex, length, this); + if (endIndex == -1) { + // We did consume the requested length + buf.readerIndex(readerIndex + length); + if ((state & HUFFMAN_COMPLETE_SHIFT) != HUFFMAN_COMPLETE_SHIFT) { + throw BAD_ENCODING; + } + return new AsciiString(dest, 0, k, false); + } + + // The process(...) method returned before the requested length was requested. This means there + // was a bad encoding detected. + buf.readerIndex(endIndex); + throw BAD_ENCODING; + } finally { + dest = null; + k = 0; + state = 0; + } + } + + /** + * This should never be called from anything but this class itself! + */ + @Override + public boolean process(byte input) { + return processNibble(input >> 4) && processNibble(input); + } + + private boolean processNibble(int input) { + // The high nibble of the flags byte of each row is always zero + // (low nibble after shifting row by 12), since there are only 3 flag bits + int index = state >> 12 | (input & 0x0F); + state = HUFFS[index]; + if ((state & HUFFMAN_FAIL_SHIFT) != 0) { + return false; + } + if ((state & HUFFMAN_EMIT_SYMBOL_SHIFT) != 0) { + // state is always positive so can cast without mask here + dest[k++] = (byte) state; + } + return true; + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHuffmanEncoder.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHuffmanEncoder.java new file mode 100644 index 0000000..6df0de5 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackHuffmanEncoder.java @@ -0,0 +1,176 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.util.AsciiString; +import io.netty.util.ByteProcessor; +import io.netty.util.internal.ObjectUtil; + +final class QpackHuffmanEncoder { + + private final int[] codes; + private final byte[] lengths; + private final EncodedLengthProcessor encodedLengthProcessor = new EncodedLengthProcessor(); + private final EncodeProcessor encodeProcessor = new EncodeProcessor(); + + QpackHuffmanEncoder() { + this(QpackUtil.HUFFMAN_CODES, QpackUtil.HUFFMAN_CODE_LENGTHS); + } + + /** + * Creates a new Huffman encoder with the specified Huffman coding. + * + * @param codes the Huffman codes indexed by symbol + * @param lengths the length of each Huffman code + */ + private QpackHuffmanEncoder(int[] codes, byte[] lengths) { + this.codes = codes; + this.lengths = lengths; + } + + /** + * Compresses the input string literal using the Huffman coding. + * + * @param out the output stream for the compressed data + * @param data the string literal to be Huffman encoded + */ + public void encode(ByteBuf out, CharSequence data) { + ObjectUtil.checkNotNull(out, "out"); + if (data instanceof AsciiString) { + AsciiString string = (AsciiString) data; + try { + encodeProcessor.out = out; + string.forEachByte(encodeProcessor); + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + encodeProcessor.end(); + } + } else { + encodeSlowPath(out, data); + } + } + + private void encodeSlowPath(ByteBuf out, CharSequence data) { + long current = 0; + int n = 0; + + for (int i = 0; i < data.length(); i++) { + int b = data.charAt(i) & 0xFF; + int code = codes[b]; + int nbits = lengths[b]; + + current <<= nbits; + current |= code; + n += nbits; + + while (n >= 8) { + n -= 8; + out.writeByte((int) (current >> n)); + } + } + + if (n > 0) { + current <<= 8 - n; + current |= 0xFF >>> n; // this should be EOS symbol + out.writeByte((int) current); + } + } + + /** + * Returns the number of bytes required to Huffman encode the input string literal. + * + * @param data the string literal to be Huffman encoded + * @return the number of bytes required to Huffman encode {@code data} + */ + int getEncodedLength(CharSequence data) { + if (data instanceof AsciiString) { + AsciiString string = (AsciiString) data; + try { + encodedLengthProcessor.reset(); + string.forEachByte(encodedLengthProcessor); + return encodedLengthProcessor.length(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } else { + return getEncodedLengthSlowPath(data); + } + } + + private int getEncodedLengthSlowPath(CharSequence data) { + long len = 0; + for (int i = 0; i < data.length(); i++) { + len += lengths[data.charAt(i) & 0xFF]; + } + return (int) ((len + 7) >> 3); + } + + private final class EncodeProcessor implements ByteProcessor { + ByteBuf out; + private long current; + private int n; + + @Override + public boolean process(byte value) { + int b = value & 0xFF; + int nbits = lengths[b]; + + current <<= nbits; + current |= codes[b]; + n += nbits; + + while (n >= 8) { + n -= 8; + out.writeByte((int) (current >> n)); + } + return true; + } + + void end() { + try { + if (n > 0) { + current <<= 8 - n; + current |= 0xFF >>> n; // this should be EOS symbol + out.writeByte((int) current); + } + } finally { + out = null; + current = 0; + n = 0; + } + } + } + + private final class EncodedLengthProcessor implements ByteProcessor { + private long len; + + @Override + public boolean process(byte value) { + len += lengths[value & 0xFF]; + return true; + } + + void reset() { + len = 0; + } + + int length() { + return (int) ((len + 7) >> 3); + } + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackStaticTable.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackStaticTable.java new file mode 100644 index 0000000..c9aa866 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackStaticTable.java @@ -0,0 +1,227 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.handler.codec.UnsupportedValueConverter; +import io.netty.util.AsciiString; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +final class QpackStaticTable { + + static final int NOT_FOUND = -1; + + /** + * Special mask used to disambiguate exact pair index from + * name only index and avoid executing lookup twice. Supposed + * to be used internally. The value should be large enough + * not to override bits from static table index (current size + * of the table is 99 elements). + */ + static final int MASK_NAME_REF = 1 << 10; + + /** + * > STATIC_INDEX_BY_NAME = createMap(length); + + private static QpackHeaderField newEmptyHeaderField(String name) { + return new QpackHeaderField(AsciiString.cached(name), AsciiString.EMPTY_STRING); + } + + private static QpackHeaderField newHeaderField(String name, String value) { + return new QpackHeaderField(AsciiString.cached(name), AsciiString.cached(value)); + } + + /** + * Return the header field at the given index value. + * Note that QPACK uses 0-based indexing when HPACK is using 1-based. + */ + static QpackHeaderField getField(int index) { + return STATIC_TABLE.get(index); + } + + /** + * Returns the lowest index value for the given header field name in the static + * table. Returns -1 if the header field name is not in the static table. + */ + static int getIndex(CharSequence name) { + List index = STATIC_INDEX_BY_NAME.get(name); + if (index == null) { + return NOT_FOUND; + } + + return index.get(0); + } + + /** + * Returns: + * a) the index value for the given header field in the static table (when found); + * b) the index value for a given name with a single bit masked (no exact match); + * c) -1 if name was not found in the static table. + */ + static int findFieldIndex(CharSequence name, CharSequence value) { + final List nameIndex = STATIC_INDEX_BY_NAME.get(name); + + // Early return if name not found in the table. + if (nameIndex == null) { + return NOT_FOUND; + } + + // If name was found, check all subsequence elements of the table for exact match. + for (int index: nameIndex) { + QpackHeaderField field = STATIC_TABLE.get(index); + if (QpackUtil.equalsVariableTime(value, field.value)) { + return index; + } + } + + // No exact match was found but we still can reference the name. + return nameIndex.get(0) | MASK_NAME_REF; + } + + /** + * Creates a map CharSequenceMap header name to index value to allow quick lookup. + */ + @SuppressWarnings("unchecked") + private static CharSequenceMap> createMap(int length) { + CharSequenceMap> mapping = + new CharSequenceMap>(true, UnsupportedValueConverter.>instance(), length); + for (int index = 0; index < length; index++) { + final QpackHeaderField field = getField(index); + final List cursor = mapping.get(field.name); + if (cursor == null) { + final List holder = new ArrayList<>(16); + holder.add(index); + mapping.set(field.name, holder); + } else { + cursor.add(index); + } + } + return mapping; + } + + private QpackStaticTable() { + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackUtil.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackUtil.java new file mode 100644 index 0000000..541748e --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/QpackUtil.java @@ -0,0 +1,468 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.util.AsciiString; +import io.netty.util.internal.ConstantTimeUtils; +import io.netty.util.internal.PlatformDependent; + +import static io.netty.util.internal.ObjectUtil.checkInRange; +import static java.lang.Math.floorDiv; + +final class QpackUtil { + private static final QpackException PREFIXED_INTEGER_TOO_LONG = + QpackException.newStatic(QpackDecoder.class, "toIntOrThrow(...)", + "QPACK - invalid prefixed integer"); + + /** + * Encode integer according to + * Section 5.1. + */ + static void encodePrefixedInteger(ByteBuf out, byte mask, int prefixLength, long toEncode) { + checkInRange(toEncode, 0, MAX_UNSIGNED_INT, "toEncode"); + int nbits = (1 << prefixLength) - 1; + if (toEncode < nbits) { + out.writeByte((byte) (mask | toEncode)); + } else { + out.writeByte((byte) (mask | nbits)); + long remainder = toEncode - nbits; + while (remainder > 128) { + byte next = (byte) ((remainder % 128) | 0x80); + out.writeByte(next); + remainder = remainder / 128; + } + out.writeByte((byte) remainder); + } + } + + /** + * Decode the integer or return {@code -1} if not enough bytes are readable. + * This method increases the readerIndex when the integer could be decoded. + * + * @param in the input {@link ByteBuf} + * @param prefixLength the prefix length + * @return the integer or {@code -1} if not enough readable bytes are in the {@link ByteBuf). + */ + static int decodePrefixedIntegerAsInt(ByteBuf in, int prefixLength) throws QpackException { + return toIntOrThrow(decodePrefixedInteger(in, prefixLength)); + } + + /** + * Converts the passed {@code aLong} to an {@code int} if the value can fit an {@code int}, otherwise throws a + * {@link QpackException}. + * + * @param aLong to convert. + * @throws QpackException If the value does not fit an {@code int}. + */ + static int toIntOrThrow(long aLong) throws QpackException { + if ((int) aLong != aLong) { + throw PREFIXED_INTEGER_TOO_LONG; + } + return (int) aLong; + } + + /** + * Decode the integer or return {@code -1} if not enough bytes are readable. + * This method increases the readerIndex when the integer could be decoded. + * + * @param in the input {@link ByteBuf} + * @param prefixLength the prefix length + * @return the integer or {@code -1} if not enough readable bytes are in the {@link ByteBuf). + */ + static long decodePrefixedInteger(ByteBuf in, int prefixLength) { + int readerIndex = in.readerIndex(); + int writerIndex = in.writerIndex(); + if (readerIndex == writerIndex) { + return -1; + } + + int nbits = (1 << prefixLength) - 1; + int first = in.readByte() & nbits; + if (first < nbits) { + return first; + } + + int idx = readerIndex + 1; + long i = first; + int factor = 0; + byte next; + do { + if (idx == writerIndex) { + in.readerIndex(readerIndex); + return -1; + } + next = in.getByte(idx++); + i += (next & 0x7fL) << factor; + factor += 7; + } while ((next & 0x80) == 0x80); + in.readerIndex(idx); + return i; + } + + static boolean firstByteEquals(ByteBuf in, byte mask) { + return (in.getByte(in.readerIndex()) & mask) == mask; + } + + /** + * Compare two {@link CharSequence} objects without leaking timing information. + *

+ * The {@code int} return type is intentional and is designed to allow cascading of constant time operations: + *

+     *     String s1 = "foo";
+     *     String s2 = "foo";
+     *     String s3 = "foo";
+     *     String s4 = "goo";
+     *     boolean equals = (equalsConstantTime(s1, s2) & equalsConstantTime(s3, s4)) != 0;
+     * 
+ * @param s1 the first value. + * @param s2 the second value. + * @return {@code 0} if not equal. {@code 1} if equal. + */ + static int equalsConstantTime(CharSequence s1, CharSequence s2) { + if (s1 instanceof AsciiString && s2 instanceof AsciiString) { + if (s1.length() != s2.length()) { + return 0; + } + AsciiString s1Ascii = (AsciiString) s1; + AsciiString s2Ascii = (AsciiString) s2; + return PlatformDependent.equalsConstantTime(s1Ascii.array(), s1Ascii.arrayOffset(), + s2Ascii.array(), s2Ascii.arrayOffset(), s1.length()); + } + + return ConstantTimeUtils.equalsConstantTime(s1, s2); + } + + /** + * Compare two {@link CharSequence}s. + * @param s1 the first value. + * @param s2 the second value. + * @return {@code false} if not equal. {@code true} if equal. + */ + static boolean equalsVariableTime(CharSequence s1, CharSequence s2) { + return AsciiString.contentEquals(s1, s2); + } + + /** + * Calculate the MaxEntries based on + * RFC9204 Section 4.5.1.1. + * + * @param maxTableCapacity the maximum table capacity. + * @return maxEntries. + */ + static long maxEntries(long maxTableCapacity) { + // MaxEntries = floor( MaxTableCapacity / 32 ) + return floorDiv(maxTableCapacity, 32); + } + + // Section 6.2. Literal Header Field Representation + enum IndexType { + INCREMENTAL, // Section 6.2.1. Literal Header Field with Incremental Indexing + NONE, // Section 6.2.2. Literal Header Field without Indexing + NEVER // Section 6.2.3. Literal Header Field never Indexed + } + + // Appendix B: Huffman Codes + // https://tools.ietf.org/html/rfc7541#appendix-B + static final int[] HUFFMAN_CODES = { + 0x1ff8, + 0x7fffd8, + 0xfffffe2, + 0xfffffe3, + 0xfffffe4, + 0xfffffe5, + 0xfffffe6, + 0xfffffe7, + 0xfffffe8, + 0xffffea, + 0x3ffffffc, + 0xfffffe9, + 0xfffffea, + 0x3ffffffd, + 0xfffffeb, + 0xfffffec, + 0xfffffed, + 0xfffffee, + 0xfffffef, + 0xffffff0, + 0xffffff1, + 0xffffff2, + 0x3ffffffe, + 0xffffff3, + 0xffffff4, + 0xffffff5, + 0xffffff6, + 0xffffff7, + 0xffffff8, + 0xffffff9, + 0xffffffa, + 0xffffffb, + 0x14, + 0x3f8, + 0x3f9, + 0xffa, + 0x1ff9, + 0x15, + 0xf8, + 0x7fa, + 0x3fa, + 0x3fb, + 0xf9, + 0x7fb, + 0xfa, + 0x16, + 0x17, + 0x18, + 0x0, + 0x1, + 0x2, + 0x19, + 0x1a, + 0x1b, + 0x1c, + 0x1d, + 0x1e, + 0x1f, + 0x5c, + 0xfb, + 0x7ffc, + 0x20, + 0xffb, + 0x3fc, + 0x1ffa, + 0x21, + 0x5d, + 0x5e, + 0x5f, + 0x60, + 0x61, + 0x62, + 0x63, + 0x64, + 0x65, + 0x66, + 0x67, + 0x68, + 0x69, + 0x6a, + 0x6b, + 0x6c, + 0x6d, + 0x6e, + 0x6f, + 0x70, + 0x71, + 0x72, + 0xfc, + 0x73, + 0xfd, + 0x1ffb, + 0x7fff0, + 0x1ffc, + 0x3ffc, + 0x22, + 0x7ffd, + 0x3, + 0x23, + 0x4, + 0x24, + 0x5, + 0x25, + 0x26, + 0x27, + 0x6, + 0x74, + 0x75, + 0x28, + 0x29, + 0x2a, + 0x7, + 0x2b, + 0x76, + 0x2c, + 0x8, + 0x9, + 0x2d, + 0x77, + 0x78, + 0x79, + 0x7a, + 0x7b, + 0x7ffe, + 0x7fc, + 0x3ffd, + 0x1ffd, + 0xffffffc, + 0xfffe6, + 0x3fffd2, + 0xfffe7, + 0xfffe8, + 0x3fffd3, + 0x3fffd4, + 0x3fffd5, + 0x7fffd9, + 0x3fffd6, + 0x7fffda, + 0x7fffdb, + 0x7fffdc, + 0x7fffdd, + 0x7fffde, + 0xffffeb, + 0x7fffdf, + 0xffffec, + 0xffffed, + 0x3fffd7, + 0x7fffe0, + 0xffffee, + 0x7fffe1, + 0x7fffe2, + 0x7fffe3, + 0x7fffe4, + 0x1fffdc, + 0x3fffd8, + 0x7fffe5, + 0x3fffd9, + 0x7fffe6, + 0x7fffe7, + 0xffffef, + 0x3fffda, + 0x1fffdd, + 0xfffe9, + 0x3fffdb, + 0x3fffdc, + 0x7fffe8, + 0x7fffe9, + 0x1fffde, + 0x7fffea, + 0x3fffdd, + 0x3fffde, + 0xfffff0, + 0x1fffdf, + 0x3fffdf, + 0x7fffeb, + 0x7fffec, + 0x1fffe0, + 0x1fffe1, + 0x3fffe0, + 0x1fffe2, + 0x7fffed, + 0x3fffe1, + 0x7fffee, + 0x7fffef, + 0xfffea, + 0x3fffe2, + 0x3fffe3, + 0x3fffe4, + 0x7ffff0, + 0x3fffe5, + 0x3fffe6, + 0x7ffff1, + 0x3ffffe0, + 0x3ffffe1, + 0xfffeb, + 0x7fff1, + 0x3fffe7, + 0x7ffff2, + 0x3fffe8, + 0x1ffffec, + 0x3ffffe2, + 0x3ffffe3, + 0x3ffffe4, + 0x7ffffde, + 0x7ffffdf, + 0x3ffffe5, + 0xfffff1, + 0x1ffffed, + 0x7fff2, + 0x1fffe3, + 0x3ffffe6, + 0x7ffffe0, + 0x7ffffe1, + 0x3ffffe7, + 0x7ffffe2, + 0xfffff2, + 0x1fffe4, + 0x1fffe5, + 0x3ffffe8, + 0x3ffffe9, + 0xffffffd, + 0x7ffffe3, + 0x7ffffe4, + 0x7ffffe5, + 0xfffec, + 0xfffff3, + 0xfffed, + 0x1fffe6, + 0x3fffe9, + 0x1fffe7, + 0x1fffe8, + 0x7ffff3, + 0x3fffea, + 0x3fffeb, + 0x1ffffee, + 0x1ffffef, + 0xfffff4, + 0xfffff5, + 0x3ffffea, + 0x7ffff4, + 0x3ffffeb, + 0x7ffffe6, + 0x3ffffec, + 0x3ffffed, + 0x7ffffe7, + 0x7ffffe8, + 0x7ffffe9, + 0x7ffffea, + 0x7ffffeb, + 0xffffffe, + 0x7ffffec, + 0x7ffffed, + 0x7ffffee, + 0x7ffffef, + 0x7fffff0, + 0x3ffffee, + 0x3fffffff // EOS + }; + + static final byte[] HUFFMAN_CODE_LENGTHS = { + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, + 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, + 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, + 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5, + 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, + 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23, + 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24, + 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, + 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, + 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25, + 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, + 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, + 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26, + 30 // EOS + }; + + static final int HUFFMAN_EOS = 256; + + static final long MIN_HEADER_TABLE_SIZE = 0; + static final long MAX_UNSIGNED_INT = 0xffffffffL; + static final long MAX_HEADER_TABLE_SIZE = MAX_UNSIGNED_INT; + + private QpackUtil() { + } +} diff --git a/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/package-info.java b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/package-info.java new file mode 100644 index 0000000..bd71447 --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/io/netty/handler/codec/http3/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020 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. + */ + +/** + * HTTP/3 implementation. + */ +package io.netty.handler.codec.http3; diff --git a/netty-handler-codec-http3/src/main/java/module-info.java b/netty-handler-codec-http3/src/main/java/module-info.java new file mode 100644 index 0000000..3a59c6e --- /dev/null +++ b/netty-handler-codec-http3/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module org.xbib.io.netty.handler.codec.httpthree { + exports io.netty.handler.codec.http3; + requires org.xbib.io.netty.buffer; + requires org.xbib.io.netty.channel; + requires org.xbib.io.netty.handler.codec; + requires org.xbib.io.netty.handler.codec.http; + requires org.xbib.io.netty.handler.codec.quic; + requires org.xbib.io.netty.util; +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/AbstractHttp3FrameTypeValidationHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/AbstractHttp3FrameTypeValidationHandlerTest.java new file mode 100644 index 0000000..4f4f01d --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/AbstractHttp3FrameTypeValidationHandlerTest.java @@ -0,0 +1,180 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static io.netty.handler.codec.http3.Http3.getQpackAttributes; +import static io.netty.handler.codec.http3.Http3.setQpackAttributes; +import static io.netty.handler.codec.http3.Http3TestUtils.verifyClose; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public abstract class AbstractHttp3FrameTypeValidationHandlerTest { + + private final QuicStreamType defaultStreamType; + private final boolean isOutbound; + private final boolean isInbound; + protected EmbeddedQuicChannel parent; + protected QpackAttributes qpackAttributes; + + protected abstract ChannelHandler newHandler(boolean server); + + protected abstract List newValidFrames(); + + protected abstract List newInvalidFrames(); + + protected AbstractHttp3FrameTypeValidationHandlerTest(QuicStreamType defaultStreamType, + boolean isInbound, boolean isOutbound) { + this.defaultStreamType = defaultStreamType; + this.isInbound = isInbound; + this.isOutbound = isOutbound; + } + + static Collection data() { + return Arrays.asList(true, false); + } + + protected void setUp(boolean server) { + parent = new EmbeddedQuicChannel(server); + qpackAttributes = new QpackAttributes(parent, false); + setQpackAttributes(parent, qpackAttributes); + } + + @AfterEach + public void tearDown() { + if (parent != null) { + final QpackAttributes qpackAttributes = getQpackAttributes(parent); + if (qpackAttributes.decoderStreamAvailable()) { + assertFalse(((EmbeddedQuicStreamChannel) qpackAttributes.decoderStream()).finish()); + } + if (qpackAttributes.encoderStreamAvailable()) { + assertFalse(((EmbeddedQuicStreamChannel) qpackAttributes.encoderStream()).finish()); + } + assertFalse(parent.finish()); + } + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testValidTypeInbound(boolean server) throws Exception { + assumeTrue(isInbound); + setUp(server); + final EmbeddedQuicStreamChannel channel = newStream(defaultStreamType, newHandler(server)); + List validFrames = newValidFrames(); + for (T valid : validFrames) { + assertTrue(channel.writeInbound(valid)); + T read = channel.readInbound(); + Http3TestUtils.assertFrameSame(valid, read); + if (valid instanceof Http3SettingsFrame) { + afterSettingsFrameRead((Http3SettingsFrame) valid); + } + } + assertFalse(channel.finish()); + } + + protected void afterSettingsFrameRead(Http3SettingsFrame settingsFrame) { + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testValidTypeOutbound(boolean server) throws Exception { + assumeTrue(isOutbound); + setUp(server); + final EmbeddedQuicStreamChannel channel = newStream(defaultStreamType, newHandler(server)); + List validFrames = newValidFrames(); + for (T valid : validFrames) { + assertTrue(channel.writeOutbound(valid)); + T read = channel.readOutbound(); + Http3TestUtils.assertFrameSame(valid, read); + } + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInvalidTypeInbound(boolean server) throws Exception { + assumeTrue(isInbound); + setUp(server); + final EmbeddedQuicStreamChannel channel = newStream(defaultStreamType, newHandler(server)); + + Http3ErrorCode errorCode = inboundErrorCodeInvalid(); + List invalidFrames = newInvalidFrames(); + for (Http3Frame invalid : invalidFrames) { + Exception e = assertThrows(Exception.class, () -> channel.writeInbound(invalid)); + Http3TestUtils.assertException(errorCode, e); + + Http3TestUtils.assertFrameReleased(invalid); + } + Http3TestUtils.verifyClose(invalidFrames.size(), errorCode, parent); + assertFalse(channel.finish()); + } + + protected Http3ErrorCode inboundErrorCodeInvalid() { + return Http3ErrorCode.H3_FRAME_UNEXPECTED; + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInvalidTypeOutbound(boolean server) throws Exception { + assumeTrue(isOutbound); + setUp(server); + final EmbeddedQuicStreamChannel channel = newStream(defaultStreamType, newHandler(server)); + + List invalidFrames = newInvalidFrames(); + for (Http3Frame invalid : invalidFrames) { + Exception e = assertThrows(Exception.class, () -> channel.writeOutbound(invalid)); + Http3TestUtils.assertException(Http3ErrorCode.H3_FRAME_UNEXPECTED, e); + + Http3TestUtils.assertFrameReleased(invalid); + } + assertFalse(channel.finish()); + } + + protected EmbeddedQuicStreamChannel qPACKEncoderStream() { + assertTrue(qpackAttributes.encoderStreamAvailable()); + return (EmbeddedQuicStreamChannel) qpackAttributes.encoderStream(); + } + + protected EmbeddedQuicStreamChannel qPACKDecoderStream() { + assertTrue(qpackAttributes.decoderStreamAvailable()); + return (EmbeddedQuicStreamChannel) qpackAttributes.decoderStream(); + } + + protected void readAndReleaseStreamHeader(EmbeddedQuicStreamChannel stream) { + ByteBuf streamType = stream.readOutbound(); + assertEquals(streamType.readableBytes(), 1); + ReferenceCountUtil.release(streamType); + } + + protected EmbeddedQuicStreamChannel newStream(QuicStreamType streamType, ChannelHandler handler) + throws Exception { + return (EmbeddedQuicStreamChannel) parent.createStream(streamType, handler).get(); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/AbtractHttp3ConnectionHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/AbtractHttp3ConnectionHandlerTest.java new file mode 100644 index 0000000..80ccdd2 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/AbtractHttp3ConnectionHandlerTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class AbtractHttp3ConnectionHandlerTest { + + private final boolean server; + + protected abstract Http3ConnectionHandler newConnectionHandler(); + + protected abstract void assertBidirectionalStreamHandled(EmbeddedQuicChannel channel, + QuicStreamChannel streamChannel); + + public AbtractHttp3ConnectionHandlerTest(boolean server) { + this.server = server; + } + + @Test + public void testOpenLocalControlStream() throws Exception { + EmbeddedQuicChannel quicChannel = new EmbeddedQuicChannel(server, new ChannelDuplexHandler()); + ChannelHandlerContext ctx = quicChannel.pipeline().firstContext(); + + Http3ConnectionHandler handler = newConnectionHandler(); + handler.handlerAdded(ctx); + handler.channelRegistered(ctx); + handler.channelActive(ctx); + + final EmbeddedQuicStreamChannel localControlStream = quicChannel.localControlStream(); + assertNotNull(localControlStream); + + assertNotNull(Http3.getLocalControlStream(quicChannel)); + + handler.channelInactive(ctx); + handler.channelUnregistered(ctx); + handler.handlerRemoved(ctx); + + assertTrue(localControlStream.finishAndReleaseAll()); + } + + @Test + public void testBidirectionalStream() throws Exception { + EmbeddedQuicChannel quicChannel = new EmbeddedQuicChannel(server, new ChannelDuplexHandler()); + final EmbeddedQuicStreamChannel bidirectionalStream = + (EmbeddedQuicStreamChannel) quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelDuplexHandler()).get(); + ChannelHandlerContext ctx = quicChannel.pipeline().firstContext(); + + Http3ConnectionHandler handler = newConnectionHandler(); + handler.handlerAdded(ctx); + handler.channelRegistered(ctx); + handler.channelActive(ctx); + + final EmbeddedQuicStreamChannel localControlStream = quicChannel.localControlStream(); + assertNotNull(localControlStream); + + handler.channelRead(ctx, bidirectionalStream); + + assertBidirectionalStreamHandled(quicChannel, bidirectionalStream); + handler.channelInactive(ctx); + handler.channelUnregistered(ctx); + handler.handlerRemoved(ctx); + + assertTrue(localControlStream.finishAndReleaseAll()); + } + + @Test + public void testUnidirectionalStream() throws Exception { + EmbeddedQuicChannel quicChannel = new EmbeddedQuicChannel(server, new ChannelDuplexHandler()); + final QuicStreamChannel unidirectionalStream = + quicChannel.createStream(QuicStreamType.UNIDIRECTIONAL, new ChannelDuplexHandler()).get(); + ChannelHandlerContext ctx = quicChannel.pipeline().firstContext(); + + Http3ConnectionHandler handler = newConnectionHandler(); + handler.handlerAdded(ctx); + handler.channelRegistered(ctx); + handler.channelActive(ctx); + + final EmbeddedQuicStreamChannel localControlStream = quicChannel.localControlStream(); + assertNotNull(localControlStream); + + handler.channelRead(ctx, unidirectionalStream); + + assertNotNull(unidirectionalStream.pipeline().get(Http3UnidirectionalStreamInboundHandler.class)); + + handler.channelInactive(ctx); + handler.channelUnregistered(ctx); + handler.handlerRemoved(ctx); + + assertTrue(localControlStream.finishAndReleaseAll()); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/EmbeddedQuicChannel.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/EmbeddedQuicChannel.java new file mode 100644 index 0000000..ad06b56 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/EmbeddedQuicChannel.java @@ -0,0 +1,313 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPromise; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicChannelConfig; +import io.netty.handler.codec.quic.QuicConnectionStats; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.handler.codec.quic.QuicTransportParameters; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import javax.net.ssl.SSLEngine; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import static io.netty.util.AttributeKey.valueOf; +import static java.lang.System.arraycopy; +import static java.util.Collections.unmodifiableCollection; + +final class EmbeddedQuicChannel extends EmbeddedChannel implements QuicChannel { + private static final AttributeKey streamIdGeneratorKey = + valueOf("embedded_channel_stream_id_generator"); + private final Map peerAllowedStreams = new EnumMap<>(QuicStreamType.class); + private final AtomicBoolean closed = new AtomicBoolean(); + private final ConcurrentLinkedQueue closeErrorCodes = new ConcurrentLinkedQueue<>(); + private QuicChannelConfig config; + + EmbeddedQuicChannel(boolean server) { + this(server, new ChannelHandler[0]); + } + + EmbeddedQuicChannel(boolean server, ChannelHandler... handlers) { + super(prependChannelConsumer(channel -> channel.attr(streamIdGeneratorKey).set(new AtomicLong(server ? 1 : 0)), + handlers)); + } + + static ChannelHandler[] prependChannelConsumer(Consumer channelConsumer, + ChannelHandler... handlers) { + ChannelHandler[] toReturn = new ChannelHandler[handlers.length + 1]; + toReturn[0] = new ChannelInboundHandlerAdapter() { + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + channelConsumer.accept(ctx.channel()); + super.handlerAdded(ctx); + } + }; + arraycopy(handlers, 0, toReturn, 1, handlers.length); + return toReturn; + } + + @Override + public boolean isTimedOut() { + return false; + } + + @Override + public SSLEngine sslEngine() { + return null; + } + + @Override + public QuicChannelConfig config() { + if (config == null) { + config = new EmbeddedQuicChannelConfig(super.config()); + } + return config; + } + + @Override + public QuicChannel flush() { + super.flush(); + return this; + } + + @Override + public QuicChannel read() { + super.read(); + return this; + } + + @Override + public long peerAllowedStreams(QuicStreamType type) { + return peerAllowedStreams.getOrDefault(type, Long.MAX_VALUE); + } + + public void peerAllowedStreams(QuicStreamType type, long peerAllowedStreams) { + this.peerAllowedStreams.put(type, peerAllowedStreams); + } + + @Override + public Future createStream(QuicStreamType type, ChannelHandler handler, + Promise promise) { + final AtomicLong streamIdGenerator = attr(streamIdGeneratorKey).get(); + return promise.setSuccess(new EmbeddedQuicStreamChannel(this, true, type, + streamIdGenerator.getAndAdd(2), handler)); + } + + @Override + public ChannelFuture close(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise) { + closeErrorCodes.add(error); + if (closed.compareAndSet(false, true)) { + promise.addListener(__ -> reason.release()); + } else { + reason.release(); + } + return close(promise); + } + + @Override + public Future collectStats(Promise promise) { + return promise.setFailure( + new UnsupportedOperationException("Collect stats not supported for embedded channel.")); + } + + public EmbeddedQuicStreamChannel localControlStream() { + return (EmbeddedQuicStreamChannel) Http3.getLocalControlStream(this); + } + + @Override + public QuicTransportParameters peerTransportParameters() { + return null; + } + + Collection closeErrorCodes() { + return unmodifiableCollection(closeErrorCodes); + } + + private static final class EmbeddedQuicChannelConfig implements QuicChannelConfig { + private final ChannelConfig delegate; + + EmbeddedQuicChannelConfig(ChannelConfig delegate) { + this.delegate = delegate; + } + + @Override + public Map, Object> getOptions() { + return delegate.getOptions(); + } + + @Override + public boolean setOptions(Map, ?> map) { + return delegate.setOptions(map); + } + + @Override + public T getOption(ChannelOption channelOption) { + return delegate.getOption(channelOption); + } + + @Override + public boolean setOption(ChannelOption channelOption, T t) { + return delegate.setOption(channelOption, t); + } + + @Override + public int getConnectTimeoutMillis() { + return delegate.getConnectTimeoutMillis(); + } + + @Override + public QuicChannelConfig setConnectTimeoutMillis(int i) { + delegate.setConnectTimeoutMillis(i); + return this; + } + + @Override + @Deprecated + public int getMaxMessagesPerRead() { + return delegate.getMaxMessagesPerRead(); + } + + @Override + @Deprecated + public QuicChannelConfig setMaxMessagesPerRead(int i) { + delegate.setMaxMessagesPerRead(i); + return this; + } + + @Override + public int getWriteSpinCount() { + return delegate.getWriteSpinCount(); + } + + @Override + public QuicChannelConfig setWriteSpinCount(int i) { + delegate.setWriteSpinCount(i); + return this; + } + + @Override + public ByteBufAllocator getAllocator() { + return delegate.getAllocator(); + } + + @Override + public QuicChannelConfig setAllocator(ByteBufAllocator byteBufAllocator) { + delegate.setAllocator(byteBufAllocator); + return this; + } + + @Override + public T getRecvByteBufAllocator() { + return delegate.getRecvByteBufAllocator(); + } + + @Override + public QuicChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator recvByteBufAllocator) { + delegate.setRecvByteBufAllocator(recvByteBufAllocator); + return this; + } + + @Override + public boolean isAutoRead() { + return delegate.isAutoRead(); + } + + @Override + public QuicChannelConfig setAutoRead(boolean b) { + delegate.setAutoRead(b); + return this; + } + + @Override + public boolean isAutoClose() { + return delegate.isAutoClose(); + } + + @Override + public QuicChannelConfig setAutoClose(boolean b) { + delegate.setAutoClose(b); + return this; + } + + @Override + public int getWriteBufferHighWaterMark() { + return delegate.getWriteBufferHighWaterMark(); + } + + @Override + public QuicChannelConfig setWriteBufferHighWaterMark(int i) { + delegate.setWriteBufferHighWaterMark(i); + return this; + } + + @Override + public int getWriteBufferLowWaterMark() { + return delegate.getWriteBufferLowWaterMark(); + } + + @Override + public QuicChannelConfig setWriteBufferLowWaterMark(int i) { + delegate.setWriteBufferLowWaterMark(i); + return this; + } + + @Override + public MessageSizeEstimator getMessageSizeEstimator() { + return delegate.getMessageSizeEstimator(); + } + + @Override + public QuicChannelConfig setMessageSizeEstimator(MessageSizeEstimator messageSizeEstimator) { + delegate.setMessageSizeEstimator(messageSizeEstimator); + return this; + } + + @Override + public WriteBufferWaterMark getWriteBufferWaterMark() { + return delegate.getWriteBufferWaterMark(); + } + + @Override + public QuicChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + delegate.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/EmbeddedQuicStreamChannel.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/EmbeddedQuicStreamChannel.java new file mode 100644 index 0000000..c6f70ef --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/EmbeddedQuicStreamChannel.java @@ -0,0 +1,466 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelId; +import io.netty.channel.EventLoop; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicStreamAddress; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamChannelConfig; +import io.netty.handler.codec.quic.QuicStreamFrame; +import io.netty.handler.codec.quic.QuicStreamPriority; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.AttributeKey; + +import java.net.SocketAddress; +import java.util.Map; + +import static io.netty.util.AttributeKey.valueOf; + +final class EmbeddedQuicStreamChannel extends EmbeddedChannel implements QuicStreamChannel { + private static final AttributeKey streamIdKey = valueOf("embedded_channel_stream_id"); + private static final AttributeKey streamTypeKey = valueOf("embedded_channel_stream_type"); + private static final AttributeKey localCreatedKey = valueOf("embedded_channel_stream_local_created"); + private QuicStreamChannelConfig config; + private Integer inputShutdown; + private Integer outputShutdown; + + EmbeddedQuicStreamChannel(ChannelHandler... handlers) { + this(null, false, QuicStreamType.BIDIRECTIONAL, 0, handlers); + } + + EmbeddedQuicStreamChannel(QuicChannel parent, boolean localCreated, QuicStreamType type, + long id, ChannelHandler... handlers) { + super(parent, DefaultChannelId.newInstance(), true, false, + EmbeddedQuicChannel.prependChannelConsumer(channel -> { + channel.attr(streamIdKey).set(id); + channel.attr(streamTypeKey).set(type); + channel.attr(localCreatedKey).set(localCreated); + }, handlers)); + } + + boolean writeInboundWithFin(Object... msgs) { + shutdownInput(); + boolean written = writeInbound(msgs); + fireInputShutdownEvents(); + return written; + } + + void writeInboundFin() { + shutdownInput(); + fireInputShutdownEvents(); + } + + private void fireInputShutdownEvents() { + pipeline().fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE); + pipeline().fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE); + } + + @Override + public QuicStreamChannel flush() { + super.flush(); + return this; + } + + @Override + public QuicStreamChannel read() { + super.read(); + return this; + } + + @Override + public QuicStreamPriority priority() { + return null; + } + + @Override + public ChannelFuture updatePriority(QuicStreamPriority priority, ChannelPromise promise) { + return promise.setFailure(new UnsupportedOperationException()); + } + + @Override + public QuicStreamAddress localAddress() { + return null; + } + + @Override + public QuicStreamAddress remoteAddress() { + return null; + } + + @Override + public QuicChannel parent() { + return (QuicChannel) super.parent(); + } + + @Override + public QuicStreamChannelConfig config() { + if (config == null) { + config = new EmbeddedQuicStreamChannelConfig(super.config()); + } + return config; + } + + @Override + public boolean isLocalCreated() { + return attr(localCreatedKey).get(); + } + + @Override + public QuicStreamType type() { + return attr(streamTypeKey).get(); + } + + @Override + public long streamId() { + return attr(streamIdKey).get(); + } + + @Override + public boolean isInputShutdown() { + return inputShutdown != null; + } + + @Override + public boolean isOutputShutdown() { + return outputShutdown != null; + } + + @Override + public ChannelFuture shutdown(int i, ChannelPromise channelPromise) { + if (inputShutdown == null) { + inputShutdown = i; + } + if (outputShutdown == null) { + outputShutdown = i; + } + return channelPromise.setSuccess(); + } + + @Override + public ChannelFuture shutdownInput(int i, ChannelPromise channelPromise) { + if (inputShutdown == null) { + inputShutdown = i; + } + return channelPromise.setSuccess(); + } + + @Override + public ChannelFuture shutdownOutput(int i, ChannelPromise channelPromise) { + if (outputShutdown == null) { + outputShutdown = i; + } + return channelPromise.setSuccess(); + } + + @Override + public boolean isShutdown() { + return isInputShutdown() && isOutputShutdown(); + } + + @Override + public ChannelFuture shutdown(ChannelPromise promise) { + return shutdown(0, promise); + } + + @Override + public ChannelFuture shutdownOutput(ChannelPromise promise) { + return shutdownOutput(0, promise); + } + + Integer outputShutdownError() { + return outputShutdown; + } + + Integer inputShutdownError() { + return inputShutdown; + } + + private Unsafe unsafe; + + @Override + public Unsafe unsafe() { + if (unsafe == null) { + Unsafe superUnsafe = super.unsafe(); + unsafe = new Unsafe() { + @Override + public RecvByteBufAllocator.Handle recvBufAllocHandle() { + return superUnsafe.recvBufAllocHandle(); + } + + @Override + public SocketAddress localAddress() { + return superUnsafe.localAddress(); + } + + @Override + public SocketAddress remoteAddress() { + return superUnsafe.remoteAddress(); + } + + @Override + public void register(EventLoop eventLoop, ChannelPromise promise) { + superUnsafe.register(eventLoop, promise); + } + + @Override + public void bind(SocketAddress localAddress, ChannelPromise promise) { + superUnsafe.bind(localAddress, promise); + } + + @Override + public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + superUnsafe.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelPromise promise) { + superUnsafe.disconnect(promise); + } + + @Override + public void close(ChannelPromise promise) { + superUnsafe.close(promise); + } + + @Override + public void closeForcibly() { + superUnsafe.closeForcibly(); + } + + @Override + public void deregister(ChannelPromise promise) { + superUnsafe.deregister(promise); + } + + @Override + public void beginRead() { + superUnsafe.beginRead(); + } + + @Override + public void write(Object msg, ChannelPromise promise) { + if (msg instanceof QuicStreamFrame && ((QuicStreamFrame) msg).hasFin()) { + // Mimic the API. + promise = promise.unvoid().addListener(f -> outputShutdown = 0); + } + superUnsafe.write(msg, promise); + } + + @Override + public void flush() { + superUnsafe.flush(); + } + + @Override + public ChannelPromise voidPromise() { + return superUnsafe.voidPromise(); + } + + @Override + public ChannelOutboundBuffer outboundBuffer() { + return superUnsafe.outboundBuffer(); + } + }; + } + return unsafe; + } + + private static final class EmbeddedQuicStreamChannelConfig implements QuicStreamChannelConfig { + private final ChannelConfig config; + private boolean allowHalfClosure; + + EmbeddedQuicStreamChannelConfig(ChannelConfig config) { + this.config = config; + } + + @Override + public QuicStreamChannelConfig setReadFrames(boolean readFrames) { + return this; + } + + @Override + public boolean isReadFrames() { + return false; + } + + @Override + public QuicStreamChannelConfig setAllowHalfClosure(boolean allowHalfClosure) { + this.allowHalfClosure = allowHalfClosure; + return this; + } + + @Override + public QuicStreamChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + config.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteSpinCount(int writeSpinCount) { + config.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public QuicStreamChannelConfig setAllocator(ByteBufAllocator allocator) { + config.setAllocator(allocator); + return this; + } + + @Override + public QuicStreamChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + config.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public QuicStreamChannelConfig setAutoRead(boolean autoRead) { + config.setAutoRead(autoRead); + return this; + } + + @Override + public QuicStreamChannelConfig setAutoClose(boolean autoClose) { + config.setAutoClose(autoClose); + return this; + } + + @Override + public QuicStreamChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + config.setMessageSizeEstimator(estimator); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + config.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + config.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + config.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + config.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public boolean isAllowHalfClosure() { + return allowHalfClosure; + } + + @Override + public Map, Object> getOptions() { + return config.getOptions(); + } + + @Override + public boolean setOptions(Map, ?> options) { + return config.setOptions(options); + } + + @Override + public T getOption(ChannelOption option) { + return config.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + return config.setOption(option, value); + } + + @Override + public int getConnectTimeoutMillis() { + return config.getConnectTimeoutMillis(); + } + + @Override + public int getMaxMessagesPerRead() { + return config.getMaxMessagesPerRead(); + } + + @Override + public int getWriteSpinCount() { + return config.getWriteSpinCount(); + } + + @Override + public ByteBufAllocator getAllocator() { + return config.getAllocator(); + } + + @Override + public T getRecvByteBufAllocator() { + return config.getRecvByteBufAllocator(); + } + + @Override + public boolean isAutoRead() { + return config.isAutoRead(); + } + + @Override + public boolean isAutoClose() { + return config.isAutoClose(); + } + + @Override + public int getWriteBufferHighWaterMark() { + return config.getWriteBufferHighWaterMark(); + } + + @Override + public int getWriteBufferLowWaterMark() { + return config.getWriteBufferLowWaterMark(); + } + + @Override + public MessageSizeEstimator getMessageSizeEstimator() { + return config.getMessageSizeEstimator(); + } + + @Override + public WriteBufferWaterMark getWriteBufferWaterMark() { + return config.getWriteBufferWaterMark(); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ClientConnectionHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ClientConnectionHandlerTest.java new file mode 100644 index 0000000..141b1fd --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ClientConnectionHandlerTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.handler.codec.quic.QuicStreamChannel; + +public class Http3ClientConnectionHandlerTest extends AbtractHttp3ConnectionHandlerTest { + + public Http3ClientConnectionHandlerTest() { + super(false); + } + + @Override + protected Http3ConnectionHandler newConnectionHandler() { + return new Http3ClientConnectionHandler(); + } + + @Override + protected void assertBidirectionalStreamHandled(EmbeddedQuicChannel channel, QuicStreamChannel streamChannel) { + Http3TestUtils.verifyClose(Http3ErrorCode.H3_STREAM_CREATION_ERROR, channel); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamFrameTypeValidatorTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamFrameTypeValidatorTest.java new file mode 100644 index 0000000..44f72e7 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamFrameTypeValidatorTest.java @@ -0,0 +1,43 @@ +/* + * 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.handler.codec.http3; + +public class Http3ControlStreamFrameTypeValidatorTest extends Http3FrameTypeValidatorTest { + + @Override + protected long[] invalidFramesTypes() { + return new long[] { + Http3CodecUtils.HTTP3_DATA_FRAME_TYPE, + Http3CodecUtils.HTTP3_HEADERS_FRAME_TYPE, + Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE + }; + } + + @Override + protected long[] validFrameTypes() { + return new long[] { + Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE, + Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE, + Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE, + Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE + }; + } + + @Override + protected Http3FrameTypeValidator newValidator() { + return Http3ControlStreamFrameTypeValidator.INSTANCE; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamInboundHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamInboundHandlerTest.java new file mode 100644 index 0000000..5159e37 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamInboundHandlerTest.java @@ -0,0 +1,269 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static io.netty.handler.codec.http3.Http3TestUtils.verifyClose; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class Http3ControlStreamInboundHandlerTest extends + AbstractHttp3FrameTypeValidationHandlerTest { + + private QpackEncoder qpackEncoder; + private Http3ControlStreamOutboundHandler remoteControlStreamHandler; + + public Http3ControlStreamInboundHandlerTest() { + super(QuicStreamType.UNIDIRECTIONAL, false, false); + } + + static Collection testData() { + List config = new ArrayList<>(); + for (int a = 0; a < 2; a++) { + for (int b = 0; b < 2; b++) { + config.add(new Object[] { a == 0, b == 0 }); + } + } + return config; + } + + @Override + protected void setUp(boolean server) { + super.setUp(server); + qpackEncoder = new QpackEncoder(); + remoteControlStreamHandler = new Http3ControlStreamOutboundHandler(server, new DefaultHttp3SettingsFrame(), + new ChannelInboundHandlerAdapter()); + } + + @Override + protected void afterSettingsFrameRead(Http3SettingsFrame settingsFrame) { + if (!qpackAttributes.dynamicTableDisabled()) { + // settings frame initialize QPACK streams + readAndReleaseStreamHeader(qPACKEncoderStream()); + readAndReleaseStreamHeader(qPACKDecoderStream()); + } + } + + @Override + protected ChannelHandler newHandler(boolean server) { + return new Http3ControlStreamInboundHandler(server, new ChannelInboundHandlerAdapter(), qpackEncoder, + remoteControlStreamHandler); + } + + @Override + protected List newValidFrames() { + return Arrays.asList(new DefaultHttp3SettingsFrame(), new DefaultHttp3GoAwayFrame(0), + new DefaultHttp3MaxPushIdFrame(0), new DefaultHttp3CancelPushFrame(0)); + } + + @Override + protected List newInvalidFrames() { + return Arrays.asList(Http3TestUtils.newHttp3RequestStreamFrame(), Http3TestUtils.newHttp3PushStreamFrame()); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testInvalidFirstFrameHttp3GoAwayFrame(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + testInvalidFirstFrame(server, forwardControlFrames, new DefaultHttp3GoAwayFrame(0)); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testInvalidFirstFrameHttp3MaxPushIdFrame(boolean server, boolean forwardControlFrames) + throws Exception { + setUp(server); + testInvalidFirstFrame(server, forwardControlFrames, new DefaultHttp3MaxPushIdFrame(0)); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testInvalidFirstFrameHttp3CancelPushFrame(boolean server, boolean forwardControlFrames) + throws Exception { + setUp(server); + testInvalidFirstFrame(server, forwardControlFrames, new DefaultHttp3CancelPushFrame(0)); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testInvalidFirstFrameNonControlFrame(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + testInvalidFirstFrame(server, forwardControlFrames, () -> 9999); + } + + private void testInvalidFirstFrame(boolean server, boolean forwardControlFrames, Http3Frame frame) + throws Exception { + final EmbeddedQuicStreamChannel channel = newStream(QuicStreamType.BIDIRECTIONAL, + new Http3ControlStreamInboundHandler(server, + forwardControlFrames ? new ChannelInboundHandlerAdapter() : null, + qpackEncoder, remoteControlStreamHandler)); + + writeInvalidFrame(forwardControlFrames, Http3ErrorCode.H3_MISSING_SETTINGS, channel, frame); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_MISSING_SETTINGS, parent); + + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testValidGoAwayFrame(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + EmbeddedChannel channel = newStream(server, forwardControlFrames); + writeValidFrame(forwardControlFrames, channel, new DefaultHttp3GoAwayFrame(0)); + writeValidFrame(forwardControlFrames, channel, new DefaultHttp3GoAwayFrame(0)); + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testSecondGoAwayFrameFailsWithHigherId(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + EmbeddedChannel channel = newStream(server, forwardControlFrames); + writeValidFrame(forwardControlFrames, channel, new DefaultHttp3GoAwayFrame(0)); + writeInvalidFrame(forwardControlFrames, Http3ErrorCode.H3_ID_ERROR, channel, new DefaultHttp3GoAwayFrame(4)); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_ID_ERROR, parent); + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testGoAwayFrameIdNonRequestStream(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + EmbeddedChannel channel = newStream(server, forwardControlFrames); + if (server) { + writeValidFrame(forwardControlFrames, channel, new DefaultHttp3GoAwayFrame(3)); + } else { + writeInvalidFrame(forwardControlFrames, Http3ErrorCode.H3_FRAME_UNEXPECTED, channel, + new DefaultHttp3GoAwayFrame(3)); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_FRAME_UNEXPECTED, parent); + } + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testHttp3MaxPushIdFrames(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + EmbeddedChannel channel = newStream(server, forwardControlFrames); + if (server) { + writeValidFrame(forwardControlFrames, channel, new DefaultHttp3MaxPushIdFrame(0)); + writeValidFrame(forwardControlFrames, channel, new DefaultHttp3MaxPushIdFrame(4)); + } else { + writeInvalidFrame(forwardControlFrames, Http3ErrorCode.H3_FRAME_UNEXPECTED, channel, + new DefaultHttp3MaxPushIdFrame(4)); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_FRAME_UNEXPECTED, parent); + } + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: forwardControlFrames = {0}") + @ValueSource(booleans = { true, false }) + public void testSecondHttp3MaxPushIdFrameFailsWithSmallerId(boolean forwardControlFrames) + throws Exception { + setUp(true); + EmbeddedChannel channel = newStream(true, forwardControlFrames); + writeValidFrame(forwardControlFrames, channel, new DefaultHttp3MaxPushIdFrame(4)); + writeInvalidFrame(forwardControlFrames, Http3ErrorCode.H3_ID_ERROR, channel, new DefaultHttp3MaxPushIdFrame(0)); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_ID_ERROR, parent); + assertFalse(channel.finish()); + } + + private EmbeddedQuicStreamChannel newStream(boolean server, boolean forwardControlFrames) throws Exception { + EmbeddedQuicStreamChannel channel = newStream(QuicStreamType.UNIDIRECTIONAL, + new Http3ControlStreamInboundHandler(server, + forwardControlFrames ? new ChannelInboundHandlerAdapter() : null, + qpackEncoder, remoteControlStreamHandler)); + + // We always need to start with a settings frame. + Http3SettingsFrame settingsFrame = new DefaultHttp3SettingsFrame(); + assertEquals(forwardControlFrames, channel.writeInbound(settingsFrame)); + if (forwardControlFrames) { + Http3TestUtils.assertFrameSame(settingsFrame, channel.readInbound()); + } else { + Http3TestUtils.assertFrameReleased(settingsFrame); + } + Object streamType = qPACKEncoderStream().readOutbound(); + assertNotNull(streamType); + ReferenceCountUtil.release(streamType); + + streamType = qPACKDecoderStream().readOutbound(); + assertNotNull(streamType); + ReferenceCountUtil.release(streamType); + return channel; + } + + private void writeValidFrame(boolean forwardControlFrames, EmbeddedChannel channel, + Http3ControlStreamFrame controlStreamFrame) { + assertEquals(forwardControlFrames, channel.writeInbound(controlStreamFrame)); + if (forwardControlFrames) { + Http3TestUtils.assertFrameSame(controlStreamFrame, channel.readInbound()); + } else { + Http3TestUtils.assertFrameReleased(controlStreamFrame); + } + } + + private void writeInvalidFrame(boolean forwardControlFrames, Http3ErrorCode expectedCode, EmbeddedChannel channel, + Http3Frame frame) { + if (forwardControlFrames) { + Exception e = assertThrows(Exception.class, () -> channel.writeInbound(frame)); + Http3TestUtils.assertException(expectedCode, e); + } else { + assertFalse(channel.writeInbound(frame)); + } + Http3TestUtils.assertFrameReleased(frame); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testSecondSettingsFrameFails(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + EmbeddedChannel channel = newStream(server, forwardControlFrames); + writeInvalidFrame(forwardControlFrames, Http3ErrorCode.H3_FRAME_UNEXPECTED, channel, + new DefaultHttp3SettingsFrame()); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_FRAME_UNEXPECTED, parent); + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}, forwardControlFrames = {1}") + @MethodSource("testData") + public void testControlStreamClosed(boolean server, boolean forwardControlFrames) throws Exception { + setUp(server); + EmbeddedQuicStreamChannel channel = newStream(server, forwardControlFrames); + channel.writeInboundFin(); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_CLOSED_CRITICAL_STREAM, parent); + assertFalse(channel.finish()); + } + + @Override + protected Http3ErrorCode inboundErrorCodeInvalid() { + return Http3ErrorCode.H3_MISSING_SETTINGS; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamOutboundHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamOutboundHandlerTest.java new file mode 100644 index 0000000..f2f9d05 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ControlStreamOutboundHandlerTest.java @@ -0,0 +1,202 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http3ControlStreamOutboundHandlerTest extends + AbstractHttp3FrameTypeValidationHandlerTest { + private final Http3SettingsFrame settingsFrame = new DefaultHttp3SettingsFrame(); + + public Http3ControlStreamOutboundHandlerTest() { + super(QuicStreamType.UNIDIRECTIONAL, false, true); + } + + @Override + protected Http3ControlStreamOutboundHandler newHandler(boolean server) { + return new Http3ControlStreamOutboundHandler(server, settingsFrame, new ChannelInboundHandlerAdapter()); + } + + @Override + protected List newValidFrames() { + return Arrays.asList(new DefaultHttp3SettingsFrame(), new DefaultHttp3GoAwayFrame(0), + new DefaultHttp3MaxPushIdFrame(0), new DefaultHttp3CancelPushFrame(0)); + } + + @Override + protected List newInvalidFrames() { + return Arrays.asList(Http3TestUtils.newHttp3RequestStreamFrame(), Http3TestUtils.newHttp3PushStreamFrame()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testStreamClosedWhileParentStillActive(boolean server) throws Exception { + setUp(server); + EmbeddedChannel channel = newStream(newHandler(server)); + assertFalse(channel.finish()); + Http3TestUtils.verifyClose(1, Http3ErrorCode.H3_CLOSED_CRITICAL_STREAM, parent); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testGoAwayIdDecreaseWorks(boolean server) throws Exception { + setUp(server); + parent.close().get(); + // Let's mark the parent as inactive before we close as otherwise we will send a close frame. + EmbeddedChannel channel = newStream(newHandler(server)); + + if (server) { + writeValidFrame(channel, new DefaultHttp3GoAwayFrame(8)); + writeValidFrame(channel, new DefaultHttp3GoAwayFrame(4)); + } else { + writeValidFrame(channel, new DefaultHttp3GoAwayFrame(9)); + writeValidFrame(channel, new DefaultHttp3GoAwayFrame(5)); + } + + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testGoAwayIdIncreaseFails(boolean server) throws Exception { + setUp(server); + // Let's mark the parent as inactive before we close as otherwise we will send a close frame. + parent.close().get(); + EmbeddedChannel channel = newStream(newHandler(server)); + + if (server) { + writeValidFrame(channel, new DefaultHttp3GoAwayFrame(4)); + writeInvalidFrame(Http3ErrorCode.H3_ID_ERROR, channel, new DefaultHttp3GoAwayFrame(8)); + } else { + writeValidFrame(channel, new DefaultHttp3GoAwayFrame(1)); + writeInvalidFrame(Http3ErrorCode.H3_ID_ERROR, channel, new DefaultHttp3GoAwayFrame(3)); + } + + assertFalse(channel.finish()); + } + + @Test + public void testGoAwayIdUseInvalidId() throws Exception { + setUp(true); + parent.close().get(); + // Let's mark the parent as inactive before we close as otherwise we will send a close frame. + EmbeddedChannel channel = newStream(newHandler(true)); + + writeInvalidFrame(Http3ErrorCode.H3_ID_ERROR, channel, new DefaultHttp3GoAwayFrame(2)); + + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testPassesUnknownFrame(boolean server) throws Exception { + setUp(server); + parent.close().get(); + // Let's mark the parent as inactive before we close as otherwise we will send a close frame. + EmbeddedChannel channel = newStream(newHandler(server)); + + writeValidFrame(channel, new DefaultHttp3UnknownFrame(Http3CodecUtils.MIN_RESERVED_FRAME_TYPE, + Unpooled.buffer().writeLong(8))); + + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testMaxPushIdFailsWhenReduced(boolean server) throws Exception { + setUp(server); + parent.close().get(); + // Let's mark the parent as inactive before we close as otherwise we will send a close frame. + EmbeddedChannel channel = newStream(newHandler(server)); + + writeValidFrame(channel, new DefaultHttp3MaxPushIdFrame(8)); + writeInvalidFrame(Http3ErrorCode.H3_ID_ERROR, channel, new DefaultHttp3MaxPushIdFrame(4)); + + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testMaxPushIdCanBeIncreased(boolean server) throws Exception { + setUp(server); + parent.close().get(); + // Let's mark the parent as inactive before we close as otherwise we will send a close frame. + Http3ControlStreamOutboundHandler handler = newHandler(server); + EmbeddedChannel channel = newStream(handler); + + writeValidFrame(channel, new DefaultHttp3MaxPushIdFrame(4)); + writeValidFrame(channel, new DefaultHttp3MaxPushIdFrame(8)); + assertEquals(Long.valueOf(8), handler.sentMaxPushId()); + + assertFalse(channel.finish()); + } + + @Override + protected EmbeddedQuicStreamChannel newStream(QuicStreamType streamType, ChannelHandler handler) + throws Exception { + return newStream(handler); + } + + private EmbeddedQuicStreamChannel newStream(ChannelHandler handler) + throws Exception { + EmbeddedQuicStreamChannel channel = super.newStream(QuicStreamType.UNIDIRECTIONAL, handler); + ByteBuf buffer = channel.readOutbound(); + // Verify that we did write the control stream prefix + int len = Http3CodecUtils.numBytesForVariableLengthInteger(buffer.getByte(0)); + assertEquals(Http3CodecUtils.HTTP3_CONTROL_STREAM_TYPE, Http3CodecUtils.readVariableLengthInteger(buffer, len)); + assertFalse(buffer.isReadable()); + buffer.release(); + + Http3SettingsFrame settings = channel.readOutbound(); + assertEquals(settingsFrame, settings); + + assertNull(channel.readOutbound()); + return channel; + } + + private void writeInvalidFrame(Http3ErrorCode expectedCode, + EmbeddedChannel channel, + Http3Frame frame) { + Exception e = assertThrows(Exception.class, () -> channel.writeOutbound(frame)); + Http3TestUtils.assertException(expectedCode, e); + } + + private void writeValidFrame(EmbeddedChannel channel, Http3Frame frame) { + try { + assertTrue(channel.writeOutbound(frame)); + } finally { + ReferenceCountUtil.release(channel.readOutbound()); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameCodecTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameCodecTest.java new file mode 100644 index 0000000..c7a13df --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameCodecTest.java @@ -0,0 +1,838 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ThreadLocalRandom; + +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_MAX_LEN; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_GO_AWAY_FRAME_MAX_LEN; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_HEADERS_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_SETTINGS_FRAME_MAX_LEN; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.writeVariableLengthInteger; +import static io.netty.handler.codec.http3.Http3TestUtils.assertException; +import static io.netty.handler.codec.http3.Http3TestUtils.verifyClose; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class Http3FrameCodecTest { + private static final int MAX_HEADER_SIZE = 1024; + private QpackEncoder encoder; + private QpackDecoder decoder; + private EmbeddedQuicStreamChannel encoderStream; + private EmbeddedQuicStreamChannel decoderStream; + private EmbeddedQuicChannel parent; + private QpackEncoderHandler qpackEncoderHandler; + private QpackDecoderHandler qpackDecoderHandler; + private QpackAttributes qpackAttributes; + private long maxTableCapacity; + + public static Collection data() { + return asList( + new Object[]{true, 0, false}, + new Object[]{true, 0, true}, + new Object[]{true, 100, false}, + new Object[]{true, 100, true}, + new Object[]{false, 0, false}, + new Object[]{false, 0, true}, + new Object[]{false, 100, false}, + new Object[]{false, 100, true} + ); + } + + public static Collection dataNoFragment() { + return asList( + new Object[]{0, false}, + new Object[]{0, true}, + new Object[]{100, false}, + new Object[]{100, true} + ); + } + + private EmbeddedQuicStreamChannel codecChannel; + + private void setUp(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + parent = new EmbeddedQuicChannel(true); + qpackAttributes = new QpackAttributes(parent, false); + Http3.setQpackAttributes(parent, qpackAttributes); + final Http3SettingsFrame settings = new DefaultHttp3SettingsFrame(); + maxTableCapacity = 1024L; + settings.put(Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, maxTableCapacity); + settings.put(Http3SettingsFrame.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS, (long) maxBlockedStreams); + decoder = new QpackDecoder(maxTableCapacity, maxBlockedStreams); + decoder.setDynamicTableCapacity(maxTableCapacity); + qpackEncoderHandler = new QpackEncoderHandler(maxTableCapacity, decoder); + encoderStream = (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, + new ChannelOutboundHandlerAdapter()).get(); + encoder = new QpackEncoder(); + qpackDecoderHandler = new QpackDecoderHandler(encoder); + decoderStream = (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, + new ChannelOutboundHandlerAdapter()).get(); + qpackAttributes.whenEncoderStreamAvailable(future -> { + if (future.isSuccess()) { + encoder.configureDynamicTable(qpackAttributes, maxTableCapacity, maxBlockedStreams); + } + }); + if (!delayQpackStreams) { + setQpackStreams(); + } + codecChannel = (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInitializer() { + @Override + protected void initChannel(QuicStreamChannel ch) { + Http3RequestStreamEncodeStateValidator encStateValidator = + new Http3RequestStreamEncodeStateValidator(); + Http3RequestStreamDecodeStateValidator decStateValidator = + new Http3RequestStreamDecodeStateValidator(); + ch.pipeline().addLast(new Http3FrameCodec(Http3FrameTypeValidator.NO_VALIDATION, decoder, + MAX_HEADER_SIZE, encoder, encStateValidator, decStateValidator)); + ch.pipeline().addLast(encStateValidator); + ch.pipeline().addLast(decStateValidator); + } + }).get(); + } + + private void setQpackStreams() { + setQpackEncoderStream(); + setQpackDecoderStream(); + } + + private void setQpackEncoderStream() { + qpackAttributes.encoderStream(encoderStream); + final Object written = encoderStream.readOutbound(); + assertNotNull(written); + ReferenceCountUtil.release(written); + } + + private void setQpackDecoderStream() { + qpackAttributes.decoderStream(decoderStream); + } + + @AfterEach + public void tearDown() { + assertFalse(codecChannel.finish()); + assertFalse(decoderStream.finish()); + assertFalse(encoderStream.finish()); + assertFalse(parent.finish()); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3CancelPushFrame_63(boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3CancelPushFrame(63)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3CancelPushFrame_16383(boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3CancelPushFrame(16383)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3CancelPushFrame_1073741823( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3CancelPushFrame(1073741823)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3CancelPushFrame_4611686018427387903( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, + new DefaultHttp3CancelPushFrame(4611686018427387903L)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3DataFrame( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + byte[] bytes = new byte[1024]; + ThreadLocalRandom.current().nextBytes(bytes); + final DefaultHttp3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(new DefaultHttp3Headers()); + addRequestHeaders(headersFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, headersFrame); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, + new DefaultHttp3DataFrame(Unpooled.wrappedBuffer(bytes))); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3GoAwayFrame_63( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3GoAwayFrame(63)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3GoAwayFrame_16383( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3GoAwayFrame(16383)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3GoAwayFrame_1073741823(boolean fragmented, + int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3GoAwayFrame(1073741823)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3MaxPushIdFrame_63( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3MaxPushIdFrame(63)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3MaxPushIdFrame_16383( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3MaxPushIdFrame(16383)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3MaxPushIdFrame_1073741823( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, new DefaultHttp3MaxPushIdFrame(1073741823)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3SettingsFrame( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3SettingsFrame settingsFrame = new DefaultHttp3SettingsFrame(); + settingsFrame.put(Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, 100L); + settingsFrame.put(Http3SettingsFrame.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS, 1L); + settingsFrame.put(Http3SettingsFrame.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE, 128L); + // Ensure we can encode and decode all sizes correctly. + settingsFrame.put(63, 63L); + settingsFrame.put(16383, 16383L); + settingsFrame.put(1073741823, 1073741823L); + settingsFrame.put(4611686018427387903L, 4611686018427387903L); + testFrameEncodedAndDecoded( + fragmented, maxBlockedStreams, delayQpackStreams, settingsFrame); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3HeadersFrame( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + addRequestHeaders(headersFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, headersFrame); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3HeadersFrameWithTrailers( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + addRequestHeaders(headersFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, headersFrame); + + final DefaultHttp3HeadersFrame trailers = new DefaultHttp3HeadersFrame(); + // add an extra header to block decoding if dynamic table enabled. + trailers.headers().add("foo", "bar"); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, trailers); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3HeadersFrameWithInvalidTrailers( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + addRequestHeaders(headersFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, headersFrame); + + final DefaultHttp3HeadersFrame trailer = new DefaultHttp3HeadersFrame(); + trailer.headers().add(":method", "GET"); + assertThrows(Http3HeadersValidationException.class, + () -> testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, trailer)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3PushPromiseFrame( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3PushPromiseFrame pushPromiseFrame = new DefaultHttp3PushPromiseFrame(9); + addRequestHeaders(pushPromiseFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, pushPromiseFrame); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testMultipleHttp3PushPromiseFrame( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3PushPromiseFrame pushPromiseFrame = new DefaultHttp3PushPromiseFrame(9); + addRequestHeaders(pushPromiseFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, pushPromiseFrame); + + Http3PushPromiseFrame pushPromiseFrame2 = new DefaultHttp3PushPromiseFrame(10); + addRequestHeaders(pushPromiseFrame2.headers()); + // add an extra header to block decoding if dynamic table enabled. + pushPromiseFrame2.headers().add("foo", "bar"); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, pushPromiseFrame2); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testMultipleHttp3PushPromiseFrameWithInvalidHeaders( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3PushPromiseFrame pushPromiseFrame = new DefaultHttp3PushPromiseFrame(9); + addRequestHeaders(pushPromiseFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, pushPromiseFrame); + + Http3PushPromiseFrame pushPromiseFrame2 = new DefaultHttp3PushPromiseFrame(10); + assertThrows(Http3HeadersValidationException.class, + () -> testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, pushPromiseFrame2)); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testHttp3UnknownFrame( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, + new DefaultHttp3UnknownFrame(Http3CodecUtils.MIN_RESERVED_FRAME_TYPE, Unpooled.buffer().writeLong(8))); + } + + // Reserved types that were used in HTTP/2 and should close the connection with an error + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testDecodeReservedFrameType0x2(boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedFrameType(fragmented, 0x2); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testDecodeReservedFrameType0x6(boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedFrameType(fragmented, 0x6); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testDecodeReservedFrameType0x8(boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedFrameType(fragmented, 0x8); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testDecodeReservedFrameType0x9(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedFrameType(delayQpackStreams, 0x9); + } + + private void testDecodeReservedFrameType(boolean delayQpackStreams, long type) { + ByteBuf buffer = Unpooled.buffer(); + Http3CodecUtils.writeVariableLengthInteger(buffer, type); + + try { + assertFalse(codecChannel.writeInbound(buffer)); + if (delayQpackStreams) { + setQpackStreams(); + codecChannel.checkException(); + } + fail(); + } catch (Exception e) { + assertException(Http3ErrorCode.H3_FRAME_UNEXPECTED, e); + } + verifyClose(Http3ErrorCode.H3_FRAME_UNEXPECTED, parent); + assertEquals(0, buffer.refCnt()); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedFrameType0x2(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedFrameType(delayQpackStreams, 0x2); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedFrameType0x6(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedFrameType(delayQpackStreams, 0x6); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedFrameType0x8(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedFrameType(delayQpackStreams, 0x8); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedFrameType0x9(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedFrameType(delayQpackStreams, 0x9); + } + + private void testEncodeReservedFrameType(boolean delayQpackStreams, long type) { + Http3UnknownFrame frame = mock(Http3UnknownFrame.class); + when(frame.type()).thenReturn(type); + when(frame.touch()).thenReturn(frame); + when(frame.touch(any())).thenReturn(frame); + try { + assertFalse(codecChannel.writeOutbound(frame)); + if (delayQpackStreams) { + setQpackStreams(); + codecChannel.checkException(); + } + fail(); + } catch (Exception e) { + assertException(Http3ErrorCode.H3_FRAME_UNEXPECTED, e); + } + // should have released the frame as well + verify(frame, times(1)).release(); + verifyClose(Http3ErrorCode.H3_FRAME_UNEXPECTED, parent); + } + + // Reserved types that were used in HTTP/2 and should close the connection with an error + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testDecodeReservedSettingsKey0x2(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedSettingsKey(delayQpackStreams, 0x2); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testDecodeReservedSettingsKey0x3(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedSettingsKey(delayQpackStreams, 0x3); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testDecodeReservedSettingsKey0x4(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedSettingsKey(delayQpackStreams, 0x4); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testDecodeReservedSettingsKey0x5(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testDecodeReservedSettingsKey(delayQpackStreams, 0x5); + } + + private void testDecodeReservedSettingsKey(boolean delayQpackStreams, long key) { + ByteBuf buffer = Unpooled.buffer(); + Http3CodecUtils.writeVariableLengthInteger(buffer, Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE); + Http3CodecUtils.writeVariableLengthInteger(buffer, 2); + // Write the key and some random value... Both should be only 1 byte long each. + Http3CodecUtils.writeVariableLengthInteger(buffer, key); + Http3CodecUtils.writeVariableLengthInteger(buffer, 1); + testDecodeInvalidSettings(delayQpackStreams, buffer); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testDecodeSettingsWithSameKey(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + ByteBuf buffer = Unpooled.buffer(); + Http3CodecUtils.writeVariableLengthInteger(buffer, Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE); + Http3CodecUtils.writeVariableLengthInteger(buffer, 4); + // Write the key and some random value... Both should be only 1 byte long each. + Http3CodecUtils.writeVariableLengthInteger(buffer, Http3SettingsFrame.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE); + Http3CodecUtils.writeVariableLengthInteger(buffer, 1); + Http3CodecUtils.writeVariableLengthInteger(buffer, Http3SettingsFrame.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE); + Http3CodecUtils.writeVariableLengthInteger(buffer, 1); + + testDecodeInvalidSettings(delayQpackStreams, buffer); + } + + private void testDecodeInvalidSettings(boolean delayQpackStreams, ByteBuf buffer) { + try { + assertFalse(codecChannel.writeInbound(buffer)); + if (delayQpackStreams) { + setQpackStreams(); + codecChannel.checkException(); + } + fail(); + } catch (Exception e) { + assertException(Http3ErrorCode.H3_SETTINGS_ERROR, e); + } + verifyClose(Http3ErrorCode.H3_SETTINGS_ERROR, parent); + assertEquals(0, buffer.refCnt()); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedSettingsKey0x2(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedSettingsKey(delayQpackStreams, 0x2); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedSettingsKey0x3(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedSettingsKey(delayQpackStreams, 0x3); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedSettingsKey0x4(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedSettingsKey(delayQpackStreams, 0x4); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testEncodeReservedSettingsKey0x5(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testEncodeReservedSettingsKey(delayQpackStreams, 0x5); + } + + private void testEncodeReservedSettingsKey(boolean delayQpackStreams, long key) { + Http3SettingsFrame frame = mock(Http3SettingsFrame.class); + when(frame.iterator()).thenReturn(Collections.singletonMap(key, 0L).entrySet().iterator()); + try { + assertFalse(codecChannel.writeOutbound(frame)); + if (delayQpackStreams) { + setQpackStreams(); + codecChannel.checkException(); + } + fail(); + } catch (Exception e) { + assertException(Http3ErrorCode.H3_SETTINGS_ERROR, e); + } + verifyClose(Http3ErrorCode.H3_SETTINGS_ERROR, parent); + } + + private static void addPseudoRequestHeaders(Http3Headers headers) { + headers.add(":authority", "netty.quic"); // name only + headers.add(":path", "/"); // name & value + headers.add(":method", "GET"); // name & value with few options per name + headers.add(":scheme", "https"); + } + + private static void addRequestHeaders(Http3Headers headers) { + addPseudoRequestHeaders(headers); + headers.add("x-qpack-draft", "19"); + } + + private void testFrameEncodedAndDecoded( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams, Http3Frame frame) throws Exception { + final boolean isDataFrame = frame instanceof Http3DataFrame; + final boolean isHeaderFrame = frameContainsHeaders(frame); + + encodeFrame(delayQpackStreams, frame, isHeaderFrame); + + if (isWriteBuffered(delayQpackStreams, isHeaderFrame)) { + setQpackStreams(); + codecChannel.checkException(); + } + ByteBuf buffer = codecChannel.readOutbound(); + + if (fragmented) { + try { + do { + ByteBuf slice = buffer.readRetainedSlice( + ThreadLocalRandom.current().nextInt(buffer.readableBytes() + 1)); + boolean producedData = codecChannel.writeInbound(slice); + if (!isDataFrame) { + if (buffer.isReadable() || (isHeaderFrame && maxBlockedStreams > 0)) { + assertFalse(producedData); + } else { + assertTrue(producedData); + } + } + } while (buffer.isReadable()); + } catch (Exception e) { + if (isDataFrame) { + ReferenceCountUtil.release(frame); + } + throw e; + } finally { + buffer.release(); + } + } else { + if (maxBlockedStreams > 0 && isHeaderFrame) { + assertFalse(codecChannel.writeInbound(buffer)); + } else { + assertTrue(codecChannel.writeInbound(buffer)); + } + } + + relayQPACKEncoderInstructions(); + + final Http3Frame actualFrame; + if (isDataFrame) { + CompositeByteBuf composite = Unpooled.compositeBuffer(); + for (;;) { + Http3DataFrame dataFrame = codecChannel.readInbound(); + if (dataFrame == null) { + break; + } + composite.addComponent(true, dataFrame.content()); + } + actualFrame = new DefaultHttp3DataFrame(composite); + } else { + actualFrame = codecChannel.readInbound(); + } + Http3TestUtils.assertFrameEquals(frame, actualFrame); + } + + private boolean isWriteBuffered(boolean delayQpackStreams, boolean isHeaderFrame) { + return delayQpackStreams && !qpackAttributes.encoderStreamAvailable() && isHeaderFrame; + } + + private void encodeFrame(boolean delayQpackStreams, Http3Frame frame, boolean isHeaderFrame) { + boolean wroteData = codecChannel.writeOutbound(retainAndDuplicate(frame)); + assertEquals(!isWriteBuffered(delayQpackStreams, isHeaderFrame), wroteData); + } + + private static Http3Frame retainAndDuplicate(Http3Frame frame) { + if (frame instanceof ByteBufHolder) { + return (Http3Frame) ((ByteBufHolder) frame).retainedDuplicate(); + } + return frame; + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testMultipleFramesEncodedAndDecodedInOneBufferHeaders(int maxBlockedStreams, boolean delayQpackStreams) + throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + addRequestHeaders(headersFrame.headers()); + testMultipleFramesEncodedAndDecodedInOneBuffer(maxBlockedStreams, delayQpackStreams, headersFrame, + new DefaultHttp3DataFrame(Unpooled.buffer().writeLong(1))); + } + + @ParameterizedTest(name = "{index}: fragmented = {0}, maxBlockedStreams = {1}, delayQpackStreams = {2}") + @MethodSource("data") + public void testMultipleFramesEncodedAndDecodedInOneBufferPushPromise( + boolean fragmented, int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + final DefaultHttp3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(new DefaultHttp3Headers()); + addPseudoRequestHeaders(headersFrame.headers()); + testFrameEncodedAndDecoded(fragmented, maxBlockedStreams, delayQpackStreams, headersFrame); + + Http3PushPromiseFrame pushPromiseFrame = new DefaultHttp3PushPromiseFrame(9); + addRequestHeaders(pushPromiseFrame.headers()); + testMultipleFramesEncodedAndDecodedInOneBuffer(maxBlockedStreams, delayQpackStreams, pushPromiseFrame, + new DefaultHttp3DataFrame(Unpooled.buffer().writeLong(1))); + } + + private void testMultipleFramesEncodedAndDecodedInOneBuffer( + int maxBlockedStreams, boolean delayQpackStreams, Http3Frame first, Http3Frame second) throws Exception { + final boolean hasHeaderFrame = frameContainsHeaders(first) || frameContainsHeaders(second); + final boolean writeBuffered = isWriteBuffered(delayQpackStreams, hasHeaderFrame); + + assertEquals(!writeBuffered, codecChannel.writeOutbound(retainAndDuplicate(first))); + assertEquals(!writeBuffered, codecChannel.writeOutbound(retainAndDuplicate(second))); + + if (writeBuffered) { + setQpackStreams(); + codecChannel.checkException(); + } + + ByteBuf mergedBuffer = Unpooled.buffer(); + for (;;) { + ByteBuf buffer = codecChannel.readOutbound(); + if (buffer == null) { + break; + } + mergedBuffer.writeBytes(buffer); + buffer.release(); + } + + if (maxBlockedStreams > 0 && hasHeaderFrame) { + assertFalse(codecChannel.writeInbound(mergedBuffer)); + } else { + assertTrue(codecChannel.writeInbound(mergedBuffer)); + } + relayQPACKEncoderInstructions(); + + Http3Frame readFrame = codecChannel.readInbound(); + Http3TestUtils.assertFrameEquals(first, readFrame); + readFrame = codecChannel.readInbound(); + Http3TestUtils.assertFrameEquals(second, readFrame); + + assertFalse(codecChannel.finish()); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testInvalidHttp3MaxPushIdFrame(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testInvalidHttp3Frame0(delayQpackStreams, HTTP3_MAX_PUSH_ID_FRAME_TYPE, + HTTP3_CANCEL_PUSH_FRAME_MAX_LEN + 1, Http3ErrorCode.H3_FRAME_ERROR); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testInvalidHttp3GoAwayFrame(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testInvalidHttp3Frame0(delayQpackStreams, HTTP3_GO_AWAY_FRAME_TYPE, + HTTP3_GO_AWAY_FRAME_MAX_LEN + 1, Http3ErrorCode.H3_FRAME_ERROR); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testInvalidHttp3SettingsFrame(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testInvalidHttp3Frame0(delayQpackStreams, HTTP3_SETTINGS_FRAME_TYPE, + HTTP3_SETTINGS_FRAME_MAX_LEN + 1, Http3ErrorCode.H3_EXCESSIVE_LOAD); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testInvalidHttp3CancelPushFrame(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testInvalidHttp3Frame0(delayQpackStreams, HTTP3_CANCEL_PUSH_FRAME_TYPE, + HTTP3_CANCEL_PUSH_FRAME_MAX_LEN + 1, Http3ErrorCode.H3_FRAME_ERROR); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testInvalidHttp3HeadersFrame(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testInvalidHttp3Frame0(delayQpackStreams, HTTP3_HEADERS_FRAME_TYPE, + MAX_HEADER_SIZE + 1, Http3ErrorCode.H3_EXCESSIVE_LOAD); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testInvalidHttp3PushPromiseFrame(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + testInvalidHttp3Frame0(delayQpackStreams, HTTP3_PUSH_PROMISE_FRAME_TYPE, + MAX_HEADER_SIZE + 9, Http3ErrorCode.H3_EXCESSIVE_LOAD); + } + + @ParameterizedTest(name = "{index}: maxBlockedStreams = {0}, delayQpackStreams = {1}") + @MethodSource("dataNoFragment") + public void testSkipUnknown(int maxBlockedStreams, boolean delayQpackStreams) throws Exception { + setUp(maxBlockedStreams, delayQpackStreams); + ByteBuf buffer = Unpooled.buffer(); + writeVariableLengthInteger(buffer, 4611686018427387903L); + writeVariableLengthInteger(buffer, 10); + buffer.writeZero(10); + + assertFalse(codecChannel.writeInbound(buffer)); + } + + private void testInvalidHttp3Frame0(boolean delayQpackStreams, int type, int length, Http3ErrorCode code) { + ByteBuf buffer = Unpooled.buffer(); + writeVariableLengthInteger(buffer, type); + writeVariableLengthInteger(buffer, length); + + try { + assertFalse(codecChannel.writeInbound(buffer)); + if (delayQpackStreams) { + setQpackStreams(); + codecChannel.checkException(); + } + fail(); + } catch (Exception e) { + assertException(code, e); + } + verifyClose(code, parent); + } + + private void relayQPACKEncoderInstructions() throws Exception { + Object msg; + while ((msg = encoderStream.readOutbound()) != null) { + qpackEncoderHandler.channelRead(encoderStream.pipeline().firstContext(), msg); + } + while ((msg = decoderStream.readOutbound()) != null) { + qpackDecoderHandler.channelRead(decoderStream.pipeline().firstContext(), msg); + } + } + + private static boolean frameContainsHeaders(Http3Frame frame) { + return frame instanceof Http3HeadersFrame || frame instanceof Http3PushPromiseFrame; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameToHttpObjectCodecTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameToHttpObjectCodecTest.java new file mode 100644 index 0000000..937d434 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameToHttpObjectCodecTest.java @@ -0,0 +1,1049 @@ +/* + * 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.handler.codec.http3; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DuplexChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.EncoderException; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpScheme; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.handler.codec.quic.InsecureQuicTokenHandler; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.CharsetUtil; +import org.junit.jupiter.api.Test; + +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http3FrameToHttpObjectCodecTest { + + @Test + public void testUpgradeEmptyFullResponse() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + assertTrue(ch.writeOutbound(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK))); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void encode100ContinueAsHttp2HeadersFrameThatIsNotEndStream() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + assertTrue(ch.writeOutbound(new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE))); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("100")); + assertFalse(ch.isOutputShutdown()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void encodeNonFullHttpResponse100ContinueIsRejected() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + assertThrows(EncoderException.class, () -> ch.writeOutbound(new DefaultHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE))); + ch.finishAndReleaseAll(); + } + + @Test + public void testUpgradeNonEmptyFullResponse() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeOutbound(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, hello))); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeEmptyFullResponseWithTrailers() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + HttpHeaders trailers = response.trailingHeaders(); + trailers.set("key", "value"); + assertTrue(ch.writeOutbound(response)); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + + Http3HeadersFrame trailersFrame = ch.readOutbound(); + assertThat(trailersFrame.headers().get("key").toString(), is("value")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeNonEmptyFullResponseWithTrailers() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, hello); + HttpHeaders trailers = response.trailingHeaders(); + trailers.set("key", "value"); + assertTrue(ch.writeOutbound(response)); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + Http3HeadersFrame trailersFrame = ch.readOutbound(); + assertThat(trailersFrame.headers().get("key").toString(), is("value")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeHeaders() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + assertTrue(ch.writeOutbound(response)); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().status().toString(), is("200")); + assertFalse(ch.isOutputShutdown()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeChunk() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + HttpContent content = new DefaultHttpContent(hello); + assertTrue(ch.writeOutbound(content)); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertFalse(ch.isOutputShutdown()); + } finally { + dataFrame.release(); + } + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeEmptyEnd() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ch.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT); + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeDataEnd() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + LastHttpContent end = new DefaultLastHttpContent(hello, true); + assertTrue(ch.writeOutbound(end)); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testUpgradeDataEndWithTrailers() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + LastHttpContent trailers = new DefaultLastHttpContent(hello, true); + HttpHeaders headers = trailers.trailingHeaders(); + headers.set("key", "value"); + assertTrue(ch.writeOutbound(trailers)); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + Http3HeadersFrame headerFrame = ch.readOutbound(); + assertThat(headerFrame.headers().get("key").toString(), is("value")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeHeaders() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + Http3Headers headers = new DefaultHttp3Headers(); + headers.path("/"); + headers.method("GET"); + + assertTrue(ch.writeInbound(new DefaultHttp3HeadersFrame(headers))); + + HttpRequest request = ch.readInbound(); + assertThat(request.uri(), is("/")); + assertThat(request.method(), is(HttpMethod.GET)); + assertThat(request.protocolVersion(), is(HttpVersion.HTTP_1_1)); + assertFalse(request instanceof FullHttpRequest); + assertTrue(HttpUtil.isTransferEncodingChunked(request)); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeHeadersWithContentLength() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + Http3Headers headers = new DefaultHttp3Headers(); + headers.path("/"); + headers.method("GET"); + headers.setInt("content-length", 0); + + assertTrue(ch.writeInbound(new DefaultHttp3HeadersFrame(headers))); + + HttpRequest request = ch.readInbound(); + assertThat(request.uri(), is("/")); + assertThat(request.method(), is(HttpMethod.GET)); + assertThat(request.protocolVersion(), is(HttpVersion.HTTP_1_1)); + assertFalse(request instanceof FullHttpRequest); + assertFalse(HttpUtil.isTransferEncodingChunked(request)); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeTrailers() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + Http3Headers headers = new DefaultHttp3Headers(); + headers.set("key", "value"); + + assertTrue(ch.writeInboundWithFin(new DefaultHttp3HeadersFrame(headers))); + + LastHttpContent trailers = ch.readInbound(); + try { + assertThat(trailers.content().readableBytes(), is(0)); + assertThat(trailers.trailingHeaders().get("key"), is("value")); + assertFalse(trailers instanceof FullHttpRequest); + } finally { + trailers.release(); + } + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeData() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeInbound(new DefaultHttp3DataFrame(hello))); + + HttpContent content = ch.readInbound(); + try { + assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertFalse(content instanceof LastHttpContent); + } finally { + content.release(); + } + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDowngradeEndData() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(true)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeInboundWithFin(new DefaultHttp3DataFrame(hello))); + + HttpContent content = ch.readInbound(); + try { + assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + content.release(); + } + + LastHttpContent last = ch.readInbound(); + try { + assertFalse(last.content().isReadable()); + assertTrue(last.trailingHeaders().isEmpty()); + } finally { + last.release(); + } + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + // client-specific tests + @Test + public void testEncodeEmptyFullRequest() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + assertTrue(ch.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world"))); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("GET")); + assertThat(headers.path().toString(), is("/hello/world")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void testEncodeNonEmptyFullRequest() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeOutbound(new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.PUT, "/hello/world", hello))); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("PUT")); + assertThat(headers.path().toString(), is("/hello/world")); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeEmptyFullRequestWithTrailers() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.PUT, "/hello/world"); + + HttpHeaders trailers = request.trailingHeaders(); + trailers.set("key", "value"); + assertTrue(ch.writeOutbound(request)); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("PUT")); + assertThat(headers.path().toString(), is("/hello/world")); + + Http3HeadersFrame trailersFrame = ch.readOutbound(); + assertThat(trailersFrame.headers().get("key").toString(), is("value")); + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeNonEmptyFullRequestWithTrailers() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.PUT, "/hello/world", hello); + + HttpHeaders trailers = request.trailingHeaders(); + trailers.set("key", "value"); + assertTrue(ch.writeOutbound(request)); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("PUT")); + assertThat(headers.path().toString(), is("/hello/world")); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + Http3HeadersFrame trailersFrame = ch.readOutbound(); + assertThat(trailersFrame.headers().get("key").toString(), is("value")); + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeRequestHeaders() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world"); + assertTrue(ch.writeOutbound(request)); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("GET")); + assertThat(headers.path().toString(), is("/hello/world")); + assertFalse(ch.isOutputShutdown()); + + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeChunkAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + HttpContent content = new DefaultHttpContent(hello); + assertTrue(ch.writeOutbound(content)); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + assertFalse(ch.isOutputShutdown()); + assertThat(ch.readOutbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeEmptyEndAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ch.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT); + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeDataEndAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + LastHttpContent end = new DefaultLastHttpContent(hello, true); + assertTrue(ch.writeOutbound(end)); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeTrailersAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + LastHttpContent trailers = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, true); + HttpHeaders headers = trailers.trailingHeaders(); + headers.set("key", "value"); + assertTrue(ch.writeOutbound(trailers)); + + Http3HeadersFrame headerFrame = ch.readOutbound(); + assertThat(headerFrame.headers().get("key").toString(), is("value")); + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeDataEndWithTrailersAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + LastHttpContent trailers = new DefaultLastHttpContent(hello, true); + HttpHeaders headers = trailers.trailingHeaders(); + headers.set("key", "value"); + assertTrue(ch.writeOutbound(trailers)); + + Http3DataFrame dataFrame = ch.readOutbound(); + try { + assertThat(dataFrame.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + dataFrame.release(); + } + + Http3HeadersFrame headerFrame = ch.readOutbound(); + assertThat(headerFrame.headers().get("key").toString(), is("value")); + + assertTrue(ch.isOutputShutdown()); + assertFalse(ch.finish()); + } + + @Test + public void testEncodeFullPromiseCompletes() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ChannelFuture writeFuture = ch.writeOneOutbound(new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world")); + ch.flushOutbound(); + assertTrue(writeFuture.isSuccess()); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("GET")); + assertThat(headers.path().toString(), is("/hello/world")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void testEncodeEmptyLastPromiseCompletes() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ChannelFuture f1 = ch.writeOneOutbound(new DefaultHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world")); + ChannelFuture f2 = ch.writeOneOutbound(new DefaultLastHttpContent()); + ch.flushOutbound(); + assertTrue(f1.isSuccess()); + assertTrue(f2.isSuccess()); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("GET")); + assertThat(headers.path().toString(), is("/hello/world")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void testEncodeMultiplePromiseCompletes() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ChannelFuture f1 = ch.writeOneOutbound(new DefaultHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world")); + ChannelFuture f2 = ch.writeOneOutbound(new DefaultLastHttpContent( + Unpooled.wrappedBuffer("foo".getBytes(StandardCharsets.UTF_8)))); + ch.flushOutbound(); + assertTrue(f1.isSuccess()); + assertTrue(f2.isSuccess()); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("GET")); + assertThat(headers.path().toString(), is("/hello/world")); + assertTrue(ch.isOutputShutdown()); + + Http3DataFrame dataFrame = ch.readOutbound(); + assertEquals("foo", dataFrame.content().toString(StandardCharsets.UTF_8)); + + assertFalse(ch.finish()); + } + + @Test + public void testEncodeTrailersCompletes() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ChannelFuture f1 = ch.writeOneOutbound(new DefaultHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world")); + LastHttpContent last = new DefaultLastHttpContent( + Unpooled.wrappedBuffer("foo".getBytes(StandardCharsets.UTF_8))); + last.trailingHeaders().add("foo", "bar"); + ChannelFuture f2 = ch.writeOneOutbound(last); + ch.flushOutbound(); + assertTrue(f1.isSuccess()); + assertTrue(f2.isSuccess()); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("GET")); + assertThat(headers.path().toString(), is("/hello/world")); + assertTrue(ch.isOutputShutdown()); + + Http3DataFrame dataFrame = ch.readOutbound(); + assertEquals("foo", dataFrame.content().toString(StandardCharsets.UTF_8)); + + Http3HeadersFrame trailingHeadersFrame = ch.readOutbound(); + assertEquals("bar", trailingHeadersFrame.headers().get("foo").toString()); + + assertFalse(ch.finish()); + } + + @Test + public void testEncodeVoidPromise() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ch.writeOneOutbound(new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.POST, "/hello/world", Unpooled.wrappedBuffer(new byte[1])), + ch.voidPromise()); + ch.flushOutbound(); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + Http3DataFrame data = ch.readOutbound(); +data.release(); + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.method().toString(), is("POST")); + assertThat(headers.path().toString(), is("/hello/world")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void testEncodeCombinations() { + // this test goes through all the branches of Http3FrameToHttpObjectCodec and ensures right functionality + + for (boolean headers : new boolean[]{false, true}) { + for (boolean last : new boolean[]{false, true}) { + for (boolean nonEmptyContent : new boolean[]{false, true}) { + for (boolean hasTrailers : new boolean[]{false, true}) { + for (boolean voidPromise : new boolean[]{false, true}) { + testEncodeCombination(headers, last, nonEmptyContent, hasTrailers, voidPromise); + } + } + } + } + } + } + + /** + * @param headers Should this be an initial message, with headers ({@link HttpRequest})? + * @param last Should this be a last message ({@link LastHttpContent})? + * @param nonEmptyContent Should this message have non-empty content? + * @param hasTrailers Should this {@code last} message have trailers? + * @param voidPromise Should the write operation use a void promise? + */ + private static void testEncodeCombination( + boolean headers, + boolean last, + boolean nonEmptyContent, + boolean hasTrailers, + boolean voidPromise + ) { + ByteBuf content = nonEmptyContent ? Unpooled.wrappedBuffer(new byte[1]) : Unpooled.EMPTY_BUFFER; + HttpHeaders trailers = new DefaultHttpHeaders(); + if (hasTrailers) { + trailers.add("foo", "bar"); + } + HttpObject msg; + if (headers) { + if (last) { + msg = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.POST, "/foo", content, new DefaultHttpHeaders(), trailers); + } else { + if (hasTrailers || nonEmptyContent) { + // not supported by the netty HTTP/1 model + content.release(); + return; + } + msg = new DefaultHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.POST, "/foo", new DefaultHttpHeaders()); + } + } else { + if (last) { + msg = new DefaultLastHttpContent(content, trailers); + } else { + if (hasTrailers) { + // makes no sense + content.release(); + return; + } + msg = new DefaultHttpContent(content); + } + } + + List framePromises = new ArrayList<>(); + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel( + new ChannelOutboundHandlerAdapter() { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + framePromises.add(promise); + ctx.write(msg, ctx.voidPromise()); + } + }, + new Http3FrameToHttpObjectCodec(false) + ); + + ChannelFuture fullPromise = ch.writeOneOutbound(msg, voidPromise ? ch.voidPromise() : ch.newPromise()); + ch.flushOutbound(); + + if (headers) { + Http3HeadersFrame headersFrame = ch.readOutbound(); + assertThat(headersFrame.headers().scheme().toString(), is("https")); + assertThat(headersFrame.headers().method().toString(), is("POST")); + assertThat(headersFrame.headers().path().toString(), is("/foo")); + } + if (nonEmptyContent) { + Http3DataFrame dataFrame = ch.readOutbound(); + assertThat(dataFrame.content().readableBytes(), is(1)); + dataFrame.release(); + } else if (!headers && !hasTrailers && !last) { + ch.readOutbound().release(); + } + if (hasTrailers) { + Http3HeadersFrame trailersFrame = ch.readOutbound(); + assertThat(trailersFrame.headers().get("foo"), is("bar")); + } + // empty LastHttpContent has no data written and will complete the promise immediately + boolean anyData = hasTrailers || nonEmptyContent || headers || !last; + if (!voidPromise) { + if (anyData) { + assertFalse(fullPromise.isDone()); + } else { + // nothing to write, immediately complete + assertTrue(fullPromise.isDone()); + } + } + if (!last || anyData) { + assertFalse(ch.isOutputShutdown()); + } + for (ChannelPromise framePromise : framePromises) { + framePromise.trySuccess(); + } + if (last) { + assertTrue(ch.isOutputShutdown()); + } + if (!voidPromise) { + assertTrue(fullPromise.isDone()); + } + assertFalse(ch.finish()); + } + + @Test + public void decode100ContinueHttp2HeadersAsFullHttpResponse() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + Http3Headers headers = new DefaultHttp3Headers(); + headers.scheme(HttpScheme.HTTP.name()); + headers.status(HttpResponseStatus.CONTINUE.codeAsText()); + + assertTrue(ch.writeInbound(new DefaultHttp3HeadersFrame(headers))); + + final FullHttpResponse response = ch.readInbound(); + try { + assertThat(response.status(), is(HttpResponseStatus.CONTINUE)); + assertThat(response.protocolVersion(), is(HttpVersion.HTTP_1_1)); + } finally { + response.release(); + } + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDecodeResponseHeaders() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + Http3Headers headers = new DefaultHttp3Headers(); + headers.scheme(HttpScheme.HTTP.name()); + headers.status(HttpResponseStatus.OK.codeAsText()); + + assertTrue(ch.writeInbound(new DefaultHttp3HeadersFrame(headers))); + + HttpResponse response = ch.readInbound(); + assertThat(response.status(), is(HttpResponseStatus.OK)); + assertThat(response.protocolVersion(), is(HttpVersion.HTTP_1_1)); + assertFalse(response instanceof FullHttpResponse); + assertTrue(HttpUtil.isTransferEncodingChunked(response)); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDecodeResponseHeadersWithContentLength() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + Http3Headers headers = new DefaultHttp3Headers(); + headers.scheme(HttpScheme.HTTP.name()); + headers.status(HttpResponseStatus.OK.codeAsText()); + headers.setInt("content-length", 0); + + assertTrue(ch.writeInbound(new DefaultHttp3HeadersFrame(headers))); + + HttpResponse response = ch.readInbound(); + assertThat(response.status(), is(HttpResponseStatus.OK)); + assertThat(response.protocolVersion(), is(HttpVersion.HTTP_1_1)); + assertFalse(response instanceof FullHttpResponse); + assertFalse(HttpUtil.isTransferEncodingChunked(response)); + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDecodeResponseTrailersAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + Http3Headers headers = new DefaultHttp3Headers(); + headers.set("key", "value"); + assertTrue(ch.writeInboundWithFin(new DefaultHttp3HeadersFrame(headers))); + + LastHttpContent trailers = ch.readInbound(); + try { + assertThat(trailers.content().readableBytes(), is(0)); + assertThat(trailers.trailingHeaders().get("key"), is("value")); + assertFalse(trailers instanceof FullHttpRequest); + } finally { + trailers.release(); + } + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDecodeDataAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeInbound(new DefaultHttp3DataFrame(hello))); + + HttpContent content = ch.readInbound(); + try { + assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world")); + assertFalse(content instanceof LastHttpContent); + } finally { + content.release(); + } + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testDecodeEndDataAsClient() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + ByteBuf hello = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8); + assertTrue(ch.writeInboundWithFin(new DefaultHttp3DataFrame(hello))); + + HttpContent content = ch.readInbound(); + try { + assertThat(content.content().toString(CharsetUtil.UTF_8), is("hello world")); + } finally { + content.release(); + } + + LastHttpContent last = ch.readInbound(); + try { + assertFalse(last.content().isReadable()); + assertTrue(last.trailingHeaders().isEmpty()); + } finally { + last.release(); + } + + assertThat(ch.readInbound(), is(nullValue())); + assertFalse(ch.finish()); + } + + @Test + public void testHostTranslated() { + EmbeddedQuicStreamChannel ch = new EmbeddedQuicStreamChannel(new Http3FrameToHttpObjectCodec(false)); + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world"); + request.headers().add(HttpHeaderNames.HOST, "example.com"); + assertTrue(ch.writeOutbound(request)); + + Http3HeadersFrame headersFrame = ch.readOutbound(); + Http3Headers headers = headersFrame.headers(); + + assertThat(headers.scheme().toString(), is("https")); + assertThat(headers.authority().toString(), is("example.com")); + assertTrue(ch.isOutputShutdown()); + + assertFalse(ch.finish()); + } + + @Test + public void multipleFramesInFin() throws InterruptedException, CertificateException, ExecutionException { + EventLoopGroup group = new NioEventLoopGroup(1); + try { + Bootstrap bootstrap = new Bootstrap() + .channel(NioDatagramChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + // initialized below + } + }) + .group(group); + + SelfSignedCertificate cert = new SelfSignedCertificate(); + + Channel server = bootstrap.bind("127.0.0.1", 0).sync().channel(); + server.pipeline().addLast(Http3.newQuicServerCodecBuilder() + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .sslContext(QuicSslContextBuilder.forServer(cert.key(), null, cert.cert()) + .applicationProtocols(Http3.supportedApplicationProtocols()).build()) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(new Http3ServerConnectionHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http3HeadersFrame) { + DefaultHttp3HeadersFrame responseHeaders = new DefaultHttp3HeadersFrame(); + responseHeaders.headers().status(HttpResponseStatus.OK.codeAsText()); + ctx.write(responseHeaders, ctx.voidPromise()); + ctx.write(new DefaultHttp3DataFrame(ByteBufUtil.encodeString( + ctx.alloc(), CharBuffer.wrap("foo"), CharsetUtil.UTF_8)), + ctx.voidPromise()); + // send a fin, this also flushes + ((DuplexChannel) ctx.channel()).shutdownOutput(); + } else { + super.channelRead(ctx, msg); + } + } + })); + } + }) + .build()); + + Channel client = bootstrap.bind("127.0.0.1", 0).sync().channel(); + client.config().setAutoRead(true); + client.pipeline().addLast(Http3.newQuicClientCodecBuilder() + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .sslContext(QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build()) + .build()); + + QuicChannel quicChannel = QuicChannel.newBootstrap(client) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(QuicChannel ch) throws Exception { + ch.pipeline().addLast(new Http3ClientConnectionHandler()); + } + }) + .remoteAddress(server.localAddress()) + .localAddress(client.localAddress()) + .connect().get(); + + BlockingQueue received = new LinkedBlockingQueue<>(); + QuicStreamChannel stream = Http3.newRequestStream(quicChannel, new Http3RequestStreamInitializer() { + @Override + protected void initRequestStream(QuicStreamChannel ch) { + ch.pipeline() + .addLast(new Http3FrameToHttpObjectCodec(false)) + .addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + received.put(msg); + } + }); + } + }).get(); + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + request.headers().add(HttpHeaderNames.HOST, "localhost"); + stream.writeAndFlush(request); + + HttpResponse respHeaders = (HttpResponse) received.poll(20, TimeUnit.SECONDS); + assertThat(respHeaders.status(), is(HttpResponseStatus.OK)); + assertThat(respHeaders, not(instanceOf(LastHttpContent.class))); + HttpContent respBody = (HttpContent) received.poll(20, TimeUnit.SECONDS); + assertThat(respBody.content().toString(CharsetUtil.UTF_8), is("foo")); + respBody.release(); + + LastHttpContent last = (LastHttpContent) received.poll(20, TimeUnit.SECONDS); + last.release(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameTypeValidationHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameTypeValidationHandlerTest.java new file mode 100644 index 0000000..f546f37 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameTypeValidationHandlerTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.quic.QuicStreamType; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +abstract class Http3FrameTypeValidationHandlerTest extends + AbstractHttp3FrameTypeValidationHandlerTest { + + Http3FrameTypeValidationHandlerTest(boolean isInbound, boolean isOutbound) { + super(QuicStreamType.BIDIRECTIONAL, isInbound, isOutbound); + } + + @Override + protected ChannelHandler newHandler(boolean server) { + return new Http3FrameTypeDuplexValidationHandler<>(Http3RequestStreamFrame.class); + } + + @Override + protected List newValidFrames() { + return Collections.singletonList(Http3TestUtils.newHttp3RequestStreamFrame()); + } + + @Override + protected List newInvalidFrames() { + return Arrays.asList(Http3TestUtils.newHttp3ControlStreamFrame(), Http3TestUtils.newHttp3PushStreamFrame()); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameTypeValidatorTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameTypeValidatorTest.java new file mode 100644 index 0000000..31f3ce3 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3FrameTypeValidatorTest.java @@ -0,0 +1,42 @@ +/* + * 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.handler.codec.http3; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public abstract class Http3FrameTypeValidatorTest { + + protected abstract long[] invalidFramesTypes(); + protected abstract long[] validFrameTypes(); + + protected abstract Http3FrameTypeValidator newValidator(); + + @Test + public void testValidFrameTypes() throws Exception { + for (long validFrameType: validFrameTypes()) { + newValidator().validate(validFrameType, true); + } + } + + @Test + public void testInvalidFrameTypes() { + for (long invalidFrameType: invalidFramesTypes()) { + assertThrows(Http3Exception.class, () -> newValidator().validate(invalidFrameType, true)); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3HeadersSinkTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3HeadersSinkTest.java new file mode 100644 index 0000000..1386d5a --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3HeadersSinkTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2020 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.handler.codec.http3; + + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class Http3HeadersSinkTest { + + @Test + public void testHeaderSizeExceeded() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 32, false, false); + addMandatoryPseudoHeaders(sink, false); + + Http3Exception e = assertThrows(Http3Exception.class, () -> sink.finish()); + Http3TestUtils.assertException(Http3ErrorCode.H3_EXCESSIVE_LOAD, e); + } + + @Test + public void testHeaderSizeNotExceed() throws Exception { + Http3Headers headers = new DefaultHttp3Headers(); + Http3HeadersSink sink = new Http3HeadersSink(headers, 64, false, false); + addMandatoryPseudoHeaders(sink, false); + sink.finish(); + } + + @Test + public void testPseudoHeaderFollowsNormalHeader() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + sink.accept("name", "value"); + sink.accept(Http3Headers.PseudoHeaderName.AUTHORITY.value(), "value"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testInvalidatePseudoHeader() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + sink.accept(":invalid", "value"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testMixRequestResponsePseudoHeaders() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "value"); + sink.accept(Http3Headers.PseudoHeaderName.STATUS.value(), "value"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testValidPseudoHeadersRequest() throws Exception { + Http3Headers headers = new DefaultHttp3Headers(); + Http3HeadersSink sink = new Http3HeadersSink(headers, 512, true, false); + addMandatoryPseudoHeaders(sink, true); + sink.finish(); + } + + @Test + public void testValidPseudoHeadersResponse() throws Exception { + Http3Headers headers = new DefaultHttp3Headers(); + Http3HeadersSink sink = new Http3HeadersSink(headers, 512, true, false); + addMandatoryPseudoHeaders(sink, false); + sink.finish(); + } + + @Test + public void testDuplicatePseudoHeader() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + addMandatoryPseudoHeaders(sink, false); + sink.accept(Http3Headers.PseudoHeaderName.AUTHORITY.value(), "value"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testMandatoryPseudoHeaderMissingRequest() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "GET"); + sink.accept(Http3Headers.PseudoHeaderName.PATH.value(), "/"); + sink.accept(Http3Headers.PseudoHeaderName.SCHEME.value(), "https"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testMandatoryPseudoHeaderMissingResponse() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testInvalidPseudoHeadersForConnect() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "CONNECT"); + sink.accept(Http3Headers.PseudoHeaderName.PATH.value(), "/"); + sink.accept(Http3Headers.PseudoHeaderName.SCHEME.value(), "https"); + sink.accept(Http3Headers.PseudoHeaderName.AUTHORITY.value(), "value"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testValidPseudoHeadersForConnect() throws Exception { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, false); + sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "CONNECT"); + sink.accept(Http3Headers.PseudoHeaderName.AUTHORITY.value(), "value"); + sink.finish(); + } + + @Test + public void testTrailersWithRequestPseudoHeaders() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, true); + sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "CONNECT"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + @Test + public void testTrailersWithResponsePseudoHeaders() { + Http3HeadersSink sink = new Http3HeadersSink(new DefaultHttp3Headers(), 512, true, true); + sink.accept(Http3Headers.PseudoHeaderName.STATUS.value(), "200"); + assertThrows(Http3HeadersValidationException.class, () -> sink.finish()); + } + + private static void addMandatoryPseudoHeaders(Http3HeadersSink sink, boolean req) { + if (req) { + sink.accept(Http3Headers.PseudoHeaderName.METHOD.value(), "GET"); + sink.accept(Http3Headers.PseudoHeaderName.PATH.value(), "/"); + sink.accept(Http3Headers.PseudoHeaderName.SCHEME.value(), "https"); + sink.accept(Http3Headers.PseudoHeaderName.AUTHORITY.value(), "value"); + } else { + sink.accept(Http3Headers.PseudoHeaderName.STATUS.value(), "200"); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamFrameTypeValidatorTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamFrameTypeValidatorTest.java new file mode 100644 index 0000000..9b33770 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamFrameTypeValidatorTest.java @@ -0,0 +1,43 @@ +/* + * 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.handler.codec.http3; + +public class Http3PushStreamFrameTypeValidatorTest extends Http3FrameTypeValidatorTest { + + @Override + protected long[] invalidFramesTypes() { + return new long[] { + Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE, + Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE, + Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE, + Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE, + Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE + }; + } + + @Override + protected long[] validFrameTypes() { + return new long[] { + Http3CodecUtils.HTTP3_DATA_FRAME_TYPE, + Http3CodecUtils.HTTP3_HEADERS_FRAME_TYPE + }; + } + + @Override + protected Http3FrameTypeValidator newValidator() { + return Http3PushStreamFrameTypeValidator.INSTANCE; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamServerValidationHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamServerValidationHandlerTest.java new file mode 100644 index 0000000..35dea6e --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamServerValidationHandlerTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.quic.QuicStreamType; + +import java.util.Arrays; +import java.util.List; + +public class Http3PushStreamServerValidationHandlerTest extends + AbstractHttp3FrameTypeValidationHandlerTest { + + public Http3PushStreamServerValidationHandlerTest() { + super(QuicStreamType.UNIDIRECTIONAL, false, true); + } + + @Override + protected ChannelHandler newHandler(boolean server) { + return Http3PushStreamServerValidationHandler.INSTANCE; + } + + @Override + protected List newValidFrames() { + return Arrays.asList(new DefaultHttp3HeadersFrame(), new DefaultHttp3DataFrame(Unpooled.EMPTY_BUFFER)); + } + + @Override + protected List newInvalidFrames() { + return Arrays.asList(Http3TestUtils.newHttp3RequestStreamFrame(), Http3TestUtils.newHttp3ControlStreamFrame()); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamTest.java new file mode 100644 index 0000000..c320a1c --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3PushStreamTest.java @@ -0,0 +1,212 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutionException; + +import static io.netty.channel.ChannelFutureListener.CLOSE_ON_FAILURE; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_ID_ERROR; +import static io.netty.handler.codec.http3.Http3TestUtils.assertFrameEquals; +import static io.netty.handler.codec.http3.Http3TestUtils.verifyClose; +import static io.netty.handler.codec.quic.QuicStreamType.UNIDIRECTIONAL; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Http3PushStreamTest { + private Http3ServerConnectionHandler serverConnectionHandler; + private EmbeddedQuicChannel serverChannel; + private Http3ServerPushStreamManager pushStreamManager; + private Http3ClientConnectionHandler clientConnectionHandler; + private EmbeddedQuicChannel clientChannel; + private int maxPushId; + private ChannelHandlerContext serverControlStreamHandlerCtx; + private EmbeddedQuicStreamChannel serverLocalControlStream; + private EmbeddedQuicStreamChannel clientLocalControlStream; + + @BeforeEach + public void setUp() throws Exception { + serverConnectionHandler = new Http3ServerConnectionHandler(new ChannelDuplexHandler(), null, null, null, true); + serverChannel = new EmbeddedQuicChannel(true, serverConnectionHandler); + serverLocalControlStream = (EmbeddedQuicStreamChannel) Http3.getLocalControlStream(serverChannel); + assertNotNull(serverLocalControlStream); + serverControlStreamHandlerCtx = mock(ChannelHandlerContext.class); + when(serverControlStreamHandlerCtx.channel()).thenReturn(serverLocalControlStream); + + serverConnectionHandler.localControlStreamHandler.channelRead(serverControlStreamHandlerCtx, + new DefaultHttp3SettingsFrame()); + pushStreamManager = new Http3ServerPushStreamManager(serverChannel); + + clientConnectionHandler = new Http3ClientConnectionHandler(null, null, null, null, true); + clientChannel = new EmbeddedQuicChannel(false, clientConnectionHandler); + clientLocalControlStream = (EmbeddedQuicStreamChannel) Http3.getLocalControlStream(clientChannel); + assertNotNull(clientLocalControlStream); + + assertTrue(serverLocalControlStream.releaseOutbound()); + assertTrue(clientLocalControlStream.releaseOutbound()); + maxPushId = 0; // allow 1 push + sendMaxPushId(maxPushId); + } + + private void sendMaxPushId(int maxPushId) throws QpackException { + final DefaultHttp3MaxPushIdFrame maxPushIdFrame = new DefaultHttp3MaxPushIdFrame(maxPushId); + serverConnectionHandler.localControlStreamHandler.channelRead(serverControlStreamHandlerCtx, maxPushIdFrame); + assertTrue(serverChannel.isActive()); + + clientLocalControlStream.writeAndFlush(maxPushIdFrame).addListener(CLOSE_ON_FAILURE); + assertTrue(clientChannel.isActive()); + assertTrue(clientLocalControlStream.releaseOutbound()); + } + + @AfterEach + public void tearDown() { + assertFalse(serverLocalControlStream.finish()); + assertFalse(serverChannel.finish()); + assertFalse(clientLocalControlStream.finish()); + assertFalse(clientChannel.finish()); + } + + @Test + public void headersData() throws Exception { + testWriteAndReadFrames(Http3TestUtils.newHeadersFrameWithPseudoHeaders(), newDataFrame()); + } + + @Test + public void headersDataTrailers() throws Exception { + testWriteAndReadFrames(Http3TestUtils.newHeadersFrameWithPseudoHeaders(), newDataFrame(), + new DefaultHttp3HeadersFrame()); + } + + @Test + public void pushPromise() throws Exception { + final EmbeddedQuicStreamChannel serverStream = newServerStream(); + readStreamHeader(serverStream).release(); + try { + assertThrows(Http3Exception.class, () -> serverStream.writeOutbound(new DefaultHttp3PushPromiseFrame(1))); + } finally { + assertFalse(serverStream.finish()); + } + } + + @Test + public void invalidPushId() throws Exception { + final EmbeddedQuicStreamChannel serverStream = + (EmbeddedQuicStreamChannel) serverChannel.createStream(UNIDIRECTIONAL, + new Http3PushStreamServerInitializer(maxPushId + 1) { + @Override + protected void initPushStream(QuicStreamChannel ch) { + // noop + } + }).get(); + final EmbeddedQuicStreamChannel clientStream = newClientStreamUninitialized(); + try { + final ByteBuf streamHeader = readStreamHeader(serverStream); + assertFalse(clientStream.writeInbound(streamHeader)); + verifyClose(H3_ID_ERROR, clientChannel); + } finally { + assertFalse(serverStream.finish()); + assertFalse(clientStream.finish()); + } + } + + @Test + public void updateMaxPushId() throws Exception { + testWriteAndReadFrames(Http3TestUtils.newHeadersFrameWithPseudoHeaders(), newDataFrame()); + assertFalse(pushStreamManager.isPushAllowed()); + + sendMaxPushId(maxPushId + 1); + testWriteAndReadFrames(Http3TestUtils.newHeadersFrameWithPseudoHeaders(), newDataFrame()); + } + + private void testWriteAndReadFrames(Http3RequestStreamFrame... frames) throws Exception { + final EmbeddedQuicStreamChannel serverStream = newServerStream(); + final EmbeddedQuicStreamChannel clientStream = newClientStream(serverStream); + try { + for (Http3RequestStreamFrame frame : frames) { + writeAndReadFrame(serverStream, clientStream, frame); + } + } finally { + assertFalse(serverStream.finish()); + assertFalse(clientStream.finish()); + } + } + + private static void writeAndReadFrame(EmbeddedQuicStreamChannel serverStream, + EmbeddedQuicStreamChannel clientStream, Http3RequestStreamFrame frame) { + ReferenceCountUtil.retain(frame); // retain so that we can compare later + assertTrue(serverStream.writeOutbound(frame)); + final ByteBuf encodedFrame = serverStream.readOutbound(); + assertNotNull(encodedFrame); + assertTrue(clientStream.writeInbound(encodedFrame)); + final Http3RequestStreamFrame readFrame = clientStream.readInbound(); + assertFrameEquals(frame, readFrame); // releases both the frames. + + assertTrue(serverStream.isActive()); + assertTrue(clientStream.isActive()); + } + + private DefaultHttp3DataFrame newDataFrame() { + return new DefaultHttp3DataFrame(serverChannel.alloc().buffer(1).writeByte(1)); + } + + private EmbeddedQuicStreamChannel newServerStream() throws InterruptedException, ExecutionException { + assertTrue(pushStreamManager.isPushAllowed()); + final long pushId = pushStreamManager.reserveNextPushId(); + return (EmbeddedQuicStreamChannel) pushStreamManager.newPushStream(pushId, null).get(); + } + + private EmbeddedQuicStreamChannel newClientStream(EmbeddedQuicStreamChannel serverStream) throws Exception { + final EmbeddedQuicStreamChannel clientStream = newClientStreamUninitialized(); + ByteBuf streamHeader = readStreamHeader(serverStream); + assertFalse(clientStream.writeInbound(streamHeader)); + assertTrue(clientChannel.isActive()); + return clientStream; + } + + private EmbeddedQuicStreamChannel newClientStreamUninitialized() throws InterruptedException, ExecutionException { + return (EmbeddedQuicStreamChannel) clientChannel.createStream(UNIDIRECTIONAL, + new Http3UnidirectionalStreamInboundClientHandler( + (__, encodeState, decodeState) -> clientConnectionHandler.newCodec(encodeState, decodeState), + clientConnectionHandler.localControlStreamHandler, + clientConnectionHandler.remoteControlStreamHandler, null, + __ -> new Http3PushStreamClientInitializer() { + @Override + protected void initPushStream(QuicStreamChannel ch) { + // noop + } + }, ChannelDuplexHandler::new, ChannelDuplexHandler::new)).get(); + } + + private ByteBuf readStreamHeader(EmbeddedQuicStreamChannel serverStream) { + serverStream.flushOutbound(); // flush the stream header + ByteBuf streamHeader = serverStream.readOutbound(); + assertNotNull(streamHeader); + return streamHeader; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamFrameTypeValidatorTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamFrameTypeValidatorTest.java new file mode 100644 index 0000000..fe4e584 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamFrameTypeValidatorTest.java @@ -0,0 +1,43 @@ +/* + * 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.handler.codec.http3; + +public class Http3RequestStreamFrameTypeValidatorTest extends Http3FrameTypeValidatorTest { + + @Override + protected long[] invalidFramesTypes() { + return new long[] { + Http3CodecUtils.HTTP3_CANCEL_PUSH_FRAME_TYPE, + Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE, + Http3CodecUtils.HTTP3_MAX_PUSH_ID_FRAME_TYPE, + Http3CodecUtils.HTTP3_SETTINGS_FRAME_TYPE + }; + } + + @Override + protected long[] validFrameTypes() { + return new long[] { + Http3CodecUtils.HTTP3_DATA_FRAME_TYPE, + Http3CodecUtils.HTTP3_HEADERS_FRAME_TYPE, + Http3CodecUtils.HTTP3_PUSH_PROMISE_FRAME_TYPE + }; + } + + @Override + protected Http3FrameTypeValidator newValidator() { + return Http3RequestStreamFrameTypeValidator.INSTANCE; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamInboundHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamInboundHandlerTest.java new file mode 100644 index 0000000..60a4d1f --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamInboundHandlerTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http3RequestStreamInboundHandlerTest { + + @Test + public void testDetectLastViaUserEvent() { + EmbeddedQuicStreamChannel channel = new EmbeddedQuicStreamChannel(new TestHttp3RequestStreamInboundHandler()); + assertTrue(channel.writeInbound(new DefaultHttp3HeadersFrame())); + assertTrue(channel.writeInbound(new DefaultHttp3DataFrame(Unpooled.buffer()))); + assertTrue(channel.writeInbound(new DefaultHttp3DataFrame(Unpooled.buffer()))); + channel.pipeline().fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE); + assertFrame(channel); + assertFrame(channel); + assertFrame(channel); + assertEquals(true, channel.readInbound()); + assertFalse(channel.finish()); + } + + private void assertFrame(EmbeddedChannel channel) { + Http3Frame frame = channel.readInbound(); + assertNotNull(frame); + ReferenceCountUtil.release(frame); + } + + private static final class TestHttp3RequestStreamInboundHandler extends Http3RequestStreamInboundHandler { + + @Override + public void channelRead(ChannelHandlerContext ctx, Http3HeadersFrame frame) { + ctx.fireChannelRead(frame); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Http3DataFrame frame) { + ctx.fireChannelRead(frame); + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) { + ctx.fireChannelRead(true); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamValidationHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamValidationHandlerTest.java new file mode 100644 index 0000000..bc1e9b0 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3RequestStreamValidationHandlerTest.java @@ -0,0 +1,544 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http3.Http3ErrorCode.H3_FRAME_UNEXPECTED; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationHandler.newClientValidator; +import static io.netty.handler.codec.http3.Http3RequestStreamValidationHandler.newServerValidator; +import static io.netty.handler.codec.http3.Http3TestUtils.verifyClose; +import static io.netty.util.ReferenceCountUtil.release; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public class Http3RequestStreamValidationHandlerTest extends Http3FrameTypeValidationHandlerTest { + private final QpackDecoder decoder; + + public Http3RequestStreamValidationHandlerTest() { + super(true, true); + decoder = new QpackDecoder(0, 0); + } + + @Override + protected ChannelHandler newHandler(boolean server) { + return new ChannelInitializer() { + @Override + protected void initChannel(QuicStreamChannel ch) { + Http3RequestStreamEncodeStateValidator encStateValidator = new Http3RequestStreamEncodeStateValidator(); + Http3RequestStreamDecodeStateValidator decStateValidator = new Http3RequestStreamDecodeStateValidator(); + ch.pipeline().addLast(encStateValidator); + ch.pipeline().addLast(decStateValidator); + ch.pipeline().addLast(newServerValidator(qpackAttributes, decoder, encStateValidator, + decStateValidator)); + } + }; + } + + @Override + protected List newValidFrames() { + return Arrays.asList(new DefaultHttp3HeadersFrame(), new DefaultHttp3DataFrame(Unpooled.directBuffer()), + new DefaultHttp3UnknownFrame(Http3CodecUtils.MAX_RESERVED_FRAME_TYPE, Unpooled.buffer())); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInvalidFrameSequenceStartInbound(boolean server) throws Exception { + setUp(server); + final EmbeddedQuicStreamChannel channel = newStream(QuicStreamType.BIDIRECTIONAL, newHandler(server)); + Http3DataFrame dataFrame = new DefaultHttp3DataFrame(Unpooled.buffer()); + + Exception e = assertThrows(Exception.class, () -> channel.writeInbound(dataFrame)); + Http3TestUtils.assertException(H3_FRAME_UNEXPECTED, e); + + Http3TestUtils.verifyClose(H3_FRAME_UNEXPECTED, parent); + assertEquals(0, dataFrame.refCnt()); + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInvalidFrameSequenceEndInbound(boolean server) throws Exception { + setUp(server); + final EmbeddedQuicStreamChannel channel = newStream(QuicStreamType.BIDIRECTIONAL, newHandler(server)); + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + Http3DataFrame dataFrame = new DefaultHttp3DataFrame(Unpooled.buffer()); + Http3DataFrame dataFrame2 = new DefaultHttp3DataFrame(Unpooled.buffer()); + Http3DataFrame dataFrame3 = new DefaultHttp3DataFrame(Unpooled.buffer()); + Http3HeadersFrame trailersFrame = new DefaultHttp3HeadersFrame(); + + assertTrue(channel.writeInbound(headersFrame)); + assertTrue(channel.writeInbound(dataFrame.retainedDuplicate())); + assertTrue(channel.writeInbound(dataFrame2.retainedDuplicate())); + assertTrue(channel.writeInbound(trailersFrame)); + + Exception e = assertThrows(Exception.class, () -> channel.writeInbound(dataFrame3)); + Http3TestUtils.assertException(H3_FRAME_UNEXPECTED, e); + + Http3TestUtils.verifyClose(H3_FRAME_UNEXPECTED, parent); + assertTrue(channel.finish()); + assertEquals(0, dataFrame3.refCnt()); + + Http3TestUtils.assertFrameEquals(headersFrame, channel.readInbound()); + Http3TestUtils.assertFrameEquals(dataFrame, channel.readInbound()); + Http3TestUtils.assertFrameEquals(dataFrame2, channel.readInbound()); + Http3TestUtils.assertFrameEquals(trailersFrame, channel.readInbound()); + assertNull(channel.readInbound()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInvalidFrameSequenceStartOutbound(boolean server) throws Exception { + setUp(server); + EmbeddedQuicStreamChannel channel = newStream(QuicStreamType.BIDIRECTIONAL, newHandler(server)); + + Http3DataFrame dataFrame = new DefaultHttp3DataFrame(Unpooled.buffer()); + + Exception e = assertThrows(Exception.class, () -> channel.writeOutbound(dataFrame)); + Http3TestUtils.assertException(H3_FRAME_UNEXPECTED, e); + + assertFalse(channel.finish()); + assertEquals(0, dataFrame.refCnt()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInvalidFrameSequenceEndOutbound(boolean server) throws Exception { + setUp(server); + EmbeddedQuicStreamChannel channel = newStream(QuicStreamType.BIDIRECTIONAL, newHandler(server)); + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + Http3DataFrame dataFrame = new DefaultHttp3DataFrame(Unpooled.buffer()); + Http3DataFrame dataFrame2 = new DefaultHttp3DataFrame(Unpooled.buffer()); + Http3DataFrame dat3Frame3 = new DefaultHttp3DataFrame(Unpooled.buffer()); + Http3HeadersFrame trailersFrame = new DefaultHttp3HeadersFrame(); + assertTrue(channel.writeOutbound(headersFrame)); + assertTrue(channel.writeOutbound(dataFrame.retainedDuplicate())); + assertTrue(channel.writeOutbound(dataFrame2.retainedDuplicate())); + assertTrue(channel.writeOutbound(trailersFrame)); + + Exception e = assertThrows(Exception.class, () -> channel.writeOutbound(dat3Frame3)); + Http3TestUtils.assertException(H3_FRAME_UNEXPECTED, e); + + assertTrue(channel.finish()); + assertEquals(0, dat3Frame3.refCnt()); + + Http3TestUtils.assertFrameEquals(headersFrame, channel.readOutbound()); + Http3TestUtils.assertFrameEquals(dataFrame, channel.readOutbound()); + Http3TestUtils.assertFrameEquals(dataFrame2, channel.readOutbound()); + Http3TestUtils.assertFrameEquals(trailersFrame, channel.readOutbound()); + assertNull(channel.readOutbound()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testGoawayReceivedBeforeWritingHeaders(boolean server) throws Exception { + setUp(server); + EmbeddedQuicStreamChannel channel = newClientStream(() -> true); + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + Exception e = assertThrows(Exception.class, () -> channel.writeOutbound(headersFrame)); + Http3TestUtils.assertException(H3_FRAME_UNEXPECTED, e); + + // We should have closed the channel. + assertFalse(channel.isActive()); + assertFalse(channel.finish()); + assertNull(channel.readOutbound()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testGoawayReceivedAfterWritingHeaders(boolean server) throws Exception { + setUp(server); + AtomicBoolean goAway = new AtomicBoolean(); + EmbeddedQuicStreamChannel channel = newClientStream(goAway::get); + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + Http3DataFrame dataFrame = new DefaultHttp3DataFrame(Unpooled.buffer()); + assertTrue(channel.writeOutbound(headersFrame)); + goAway.set(true); + assertTrue(channel.writeOutbound(dataFrame.retainedDuplicate())); + assertTrue(channel.finish()); + Http3TestUtils.assertFrameEquals(headersFrame, channel.readOutbound()); + Http3TestUtils.assertFrameEquals(dataFrame, channel.readOutbound()); + + assertNull(channel.readOutbound()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testClientHeadRequestWithContentLength(boolean server) throws Exception { + setUp(server); + EmbeddedQuicStreamChannel channel = newClientStream(() -> false); + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().method(HttpMethod.HEAD.asciiName()); + assertTrue(channel.writeOutbound(headersFrame)); + + Http3HeadersFrame responseHeadersFrame = new DefaultHttp3HeadersFrame(); + responseHeadersFrame.headers().setLong(HttpHeaderNames.CONTENT_LENGTH, 10); + + assertTrue(channel.writeInbound(responseHeadersFrame)); + channel.pipeline().fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE); + + assertTrue(channel.finishAndReleaseAll()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testClientNonHeadRequestWithContentLengthNoData(boolean server) throws Exception { + setUp(server); + testClientNonHeadRequestWithContentLength(true, false); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testClientNonHeadRequestWithContentLengthNoDataAndTrailers(boolean server) throws Exception { + setUp(server); + testClientNonHeadRequestWithContentLength(true, true); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testClientNonHeadRequestWithContentLengthNotEnoughData(boolean server) throws Exception { + setUp(server); + testClientNonHeadRequestWithContentLength(false, false); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testClientNonHeadRequestWithContentLengthNotEnoughDataAndTrailer(boolean server) throws Exception { + setUp(server); + testClientNonHeadRequestWithContentLength(false, true); + } + + private void testClientNonHeadRequestWithContentLength(boolean noData, boolean trailers) throws Exception { + EmbeddedQuicStreamChannel channel = newClientStream(() -> false); + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().method(HttpMethod.GET.asciiName()); + assertTrue(channel.writeOutbound(headersFrame)); + + Http3HeadersFrame responseHeadersFrame = new DefaultHttp3HeadersFrame(); + responseHeadersFrame.headers().setLong(HttpHeaderNames.CONTENT_LENGTH, 10); + + assertTrue(channel.writeInbound(responseHeadersFrame)); + if (!noData) { + assertTrue(channel.writeInbound(new DefaultHttp3DataFrame(Unpooled.buffer().writeZero(9)))); + } + try { + if (trailers) { + channel.writeInbound(new DefaultHttp3HeadersFrame()); + } else { + channel.pipeline().fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE); + channel.checkException(); + } + } catch (Exception e) { + Http3TestUtils.assertException(Http3ErrorCode.H3_MESSAGE_ERROR, e); + } + assertTrue(channel.finishAndReleaseAll()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testServerWithContentLengthNoData(boolean server) throws Exception { + setUp(server); + testServerWithContentLength(true, false); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testServerWithContentLengthNoDataAndTrailers(boolean server) throws Exception { + setUp(server); + testServerWithContentLength(true, true); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testServerWithContentLengthNotEnoughData(boolean server) throws Exception { + setUp(server); + testServerWithContentLength(false, false); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testServerWithContentLengthNotEnoughDataAndTrailer(boolean server) throws Exception { + setUp(server); + testServerWithContentLength(false, true); + } + + private void testServerWithContentLength(boolean noData, boolean trailers) throws Exception { + EmbeddedQuicStreamChannel channel = newServerStream(); + + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().setLong(HttpHeaderNames.CONTENT_LENGTH, 10); + headersFrame.headers().method(HttpMethod.POST.asciiName()); + assertTrue(channel.writeInbound(headersFrame)); + + if (!noData) { + assertTrue(channel.writeInbound(new DefaultHttp3DataFrame(Unpooled.buffer().writeZero(9)))); + } + try { + if (trailers) { + channel.writeInbound(new DefaultHttp3HeadersFrame()); + } else { + channel.pipeline().fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE); + channel.checkException(); + } + } catch (Exception e) { + Http3TestUtils.assertException(Http3ErrorCode.H3_MESSAGE_ERROR, e); + } + assertTrue(channel.finishAndReleaseAll()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testHttp3HeadersFrameWithConnectionHeader(boolean server) throws Exception { + setUp(server); + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().add(HttpHeaderNames.CONNECTION, "something"); + testHeadersFrame(headersFrame, Http3ErrorCode.H3_MESSAGE_ERROR); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testHttp3HeadersFrameWithTeHeaderAndInvalidValue(boolean server) throws Exception { + setUp(server); + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().add(HttpHeaderNames.TE, "something"); + testHeadersFrame(headersFrame, Http3ErrorCode.H3_MESSAGE_ERROR); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testHttp3HeadersFrameWithTeHeaderAndValidValue(boolean server) throws Exception { + setUp(server); + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().add(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS); + testHeadersFrame(headersFrame, null); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponseAfterActualResponseServer(boolean server) throws Exception { + setUp(server); + testInformationalResponse(true, true, newResponse(OK), newResponse(CONTINUE)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponseAfterActualResponseClient(boolean server) throws Exception { + setUp(server); + testInformationalResponse(false, true, newResponse(OK), newResponse(CONTINUE)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testMultiInformationalResponseServer(boolean server) throws Exception { + setUp(server); + testInformationalResponse(true, false, newResponse(CONTINUE), newResponse(CONTINUE), newResponse(OK)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testMultiInformationalResponseClient(boolean server) throws Exception { + setUp(server); + testInformationalResponse(false, false, newResponse(CONTINUE), newResponse(CONTINUE), newResponse(OK)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testMultiInformationalResponseAfterActualResponseServer(boolean server) throws Exception { + setUp(server); + testInformationalResponse(true, false, newResponse(CONTINUE), newResponse(CONTINUE), newResponse(OK)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testMultiInformationalResponseAfterActualResponseClient(boolean server) throws Exception { + setUp(server); + testInformationalResponse(false, false, newResponse(CONTINUE), newResponse(CONTINUE), newResponse(OK)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponseWithDataAndTrailersServer(boolean server) throws Exception { + setUp(server); + testInformationalResponse(true, false, newResponse(CONTINUE), newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer()), + new DefaultHttp3HeadersFrame()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponseWithDataAndTrailersClient(boolean server) throws Exception { + setUp(server); + testInformationalResponse(false, false, newResponse(CONTINUE), newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer()), + new DefaultHttp3HeadersFrame()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponseWithDataServer(boolean server) throws Exception { + setUp(server); + testInformationalResponse(true, false, newResponse(CONTINUE), newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer())); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponseWithDataClient(boolean server) throws Exception { + setUp(server); + testInformationalResponse(false, false, newResponse(CONTINUE), newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer())); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponsePostDataServer(boolean server) throws Exception { + setUp(server); + testInformationalResponse(true, true, newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer()), newResponse(CONTINUE)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponsePostDataClient(boolean server) throws Exception { + setUp(server); + testInformationalResponse(false, true, newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer()), newResponse(CONTINUE)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponsePostTrailersServer(boolean server) throws Exception { + setUp(server); + testInformationalResponse(true, true, newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer()), new DefaultHttp3HeadersFrame(), newResponse(CONTINUE)); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @MethodSource("data") + public void testInformationalResponsePostTrailersClient(boolean server) throws Exception { + setUp(server); + testInformationalResponse(false, true, newResponse(OK), + new DefaultHttp3DataFrame(Unpooled.buffer()), new DefaultHttp3HeadersFrame(), newResponse(CONTINUE)); + } + + private void testInformationalResponse(boolean server, boolean expectFail, Http3Frame... frames) throws Exception { + EmbeddedQuicStreamChannel channel = server ? newServerStream() : + newClientStream(() -> false); + + for (int i = 0; i < frames.length; i++) { + Http3Frame frame = frames[i]; + Http3Frame read = null; + try { + if (server) { + assertTrue(channel.writeOutbound(frame)); + if (expectFail && i == frames.length - 1) { + fail(); + } else { + read = channel.readOutbound(); + } + } else { + assertTrue(channel.writeInbound(frame)); + if (expectFail && i == frames.length - 1) { + fail(); + } else { + read = channel.readInbound(); + } + } + assertEquals(frame, read); + } catch (Exception e) { + Http3TestUtils.assertException(H3_FRAME_UNEXPECTED, e); + if (!server) { + Http3TestUtils.verifyClose(H3_FRAME_UNEXPECTED, parent); + } + } finally { + release(read); + } + } + assertFalse(parent.finish()); + assertFalse(channel.finish()); + } + + private void testHeadersFrame(Http3HeadersFrame headersFrame, Http3ErrorCode code) throws Exception { + EmbeddedQuicStreamChannel channel = newServerStream(); + try { + assertTrue(channel.writeInbound(headersFrame)); + if (code != null) { + fail(); + } + } catch (Throwable cause) { + if (code == null) { + throw cause; + } + Http3TestUtils.assertException(code, cause); + assertEquals((Integer) code.code, channel.outputShutdownError()); + } + // Only expect produced messages when there was no error. + assertEquals(code == null, channel.finishAndReleaseAll()); + } + + private EmbeddedQuicStreamChannel newClientStream(final BooleanSupplier goAwayReceivedSupplier) throws Exception { + return newStream(QuicStreamType.BIDIRECTIONAL, new ChannelInitializer() { + @Override + protected void initChannel(QuicStreamChannel ch) { + Http3RequestStreamEncodeStateValidator encStateValidator = new Http3RequestStreamEncodeStateValidator(); + Http3RequestStreamDecodeStateValidator decStateValidator = new Http3RequestStreamDecodeStateValidator(); + ch.pipeline().addLast(encStateValidator); + ch.pipeline().addLast(decStateValidator); + ch.pipeline().addLast(newClientValidator(goAwayReceivedSupplier, qpackAttributes, decoder, + encStateValidator, decStateValidator)); + } + }); + } + + private EmbeddedQuicStreamChannel newServerStream() throws Exception { + return newStream(QuicStreamType.BIDIRECTIONAL, newHandler(true)); + } + + private static Http3Frame newResponse(HttpResponseStatus status) { + Http3HeadersFrame frame = new DefaultHttp3HeadersFrame(); + frame.headers().status(status.codeAsText()); + return frame; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ServerConnectionHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ServerConnectionHandlerTest.java new file mode 100644 index 0000000..8a66f84 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ServerConnectionHandlerTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.quic.QuicStreamChannel; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class Http3ServerConnectionHandlerTest extends AbtractHttp3ConnectionHandlerTest { + private static final ChannelHandler REQUEST_HANDLER = new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }; + + public Http3ServerConnectionHandlerTest() { + super(true); + } + + @Override + protected Http3ConnectionHandler newConnectionHandler() { + return new Http3ServerConnectionHandler(REQUEST_HANDLER); + } + + @Override + protected void assertBidirectionalStreamHandled(EmbeddedQuicChannel channel, QuicStreamChannel streamChannel) { + assertNotNull(streamChannel.pipeline().context(REQUEST_HANDLER)); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ServerPushStreamManagerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ServerPushStreamManagerTest.java new file mode 100644 index 0000000..a6406d8 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3ServerPushStreamManagerTest.java @@ -0,0 +1,208 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Promise; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import static java.util.function.UnaryOperator.identity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Http3ServerPushStreamManagerTest { + + private EmbeddedQuicChannel channel; + private Http3ServerPushStreamManager manager; + private Http3ServerConnectionHandler connectionHandler; + private ChannelHandlerContext controlStreamHandlerCtx; + private EmbeddedQuicStreamChannel localControlStream; + + @BeforeEach + public void setUp() throws Exception { + connectionHandler = new Http3ServerConnectionHandler(new Http3RequestStreamInboundHandler() { + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3HeadersFrame frame) { + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3DataFrame frame) { + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) { + // NOOP + } + }, null, null, null, true); + channel = new EmbeddedQuicChannel(true, connectionHandler); + localControlStream = (EmbeddedQuicStreamChannel) Http3.getLocalControlStream(channel); + assertNotNull(localControlStream); + assertTrue(localControlStream.releaseOutbound()); // settings + + controlStreamHandlerCtx = mock(ChannelHandlerContext.class); + when(controlStreamHandlerCtx.channel()).thenReturn(localControlStream); + connectionHandler.localControlStreamHandler.channelRead(controlStreamHandlerCtx, + new DefaultHttp3SettingsFrame()); + manager = new Http3ServerPushStreamManager(channel); + } + + @AfterEach + public void tearDown() { + assertFalse(localControlStream.finish()); + assertFalse(channel.finish()); + } + + @Test + public void pushAllowed() throws Exception { + assertFalse(manager.isPushAllowed()); + sendMaxPushId(1); + assertTrue(manager.isPushAllowed()); + } + + @Test + public void reserveWhenPushNotAllowed() { + assertThrows(IllegalStateException.class, () -> manager.reserveNextPushId()); + } + + @Test + public void reserveWhenPushAllowed() throws Exception { + sendMaxPushId(2); + assertEquals(0, manager.reserveNextPushId()); + } + + @Test + public void reservesAfterRefreshMaxId() throws Exception { + sendMaxPushId(0); + assertEquals(0, manager.reserveNextPushId()); + assertFalse(manager.isPushAllowed()); + sendMaxPushId(1); + assertEquals(1, manager.reserveNextPushId()); + } + + @Test + public void pushStreamNoHandler() throws Exception { + pushStreamCreateAndClose(pushId -> newPushStream(null, pushId)); + } + + @Test + public void pushStreamWithHandler() throws Exception { + final PushStreamListener pushStreamHandler = new PushStreamListener(); + pushStreamCreateAndClose(pushId -> newPushStream(pushStreamHandler, pushId)); + assertEquals(1, pushStreamHandler.framesWritten.size()); + assertTrue(pushStreamHandler.framesWritten.get(0) instanceof Http3HeadersFrame); + } + + @Test + public void pushStreamWithBootstrapNoHandler() throws Exception { + pushStreamWithBootstrapCreateAndClose(null); + } + + @Test + public void pushStreamWithBootstrapWithHandler() throws Exception { + final PushStreamListener pushStreamHandler = new PushStreamListener(); + pushStreamWithBootstrapCreateAndClose(pushStreamHandler); + assertEquals(1, pushStreamHandler.framesWritten.size()); + assertTrue(pushStreamHandler.framesWritten.get(0) instanceof Http3HeadersFrame); + } + + private void pushStreamWithBootstrapCreateAndClose(ChannelHandler pushStreamHandler) throws Exception { + pushStreamCreateAndClose(pushId -> newPushStreamWithBootstrap(pushStreamHandler, pushId)); + } + + private void pushStreamCreateAndClose(PushStreamFactory pushStreamFactory) throws Exception { + sendMaxPushId(1); + final long pushId = manager.reserveNextPushId(); + final EmbeddedQuicStreamChannel pushStream = pushStreamFactory.createPushStream(pushId); + + final DefaultHttp3HeadersFrame headerFrame = Http3TestUtils.newHeadersFrameWithPseudoHeaders(); + assertTrue(pushStream.writeOutbound(headerFrame)); + final ByteBuf encodedHeaders = pushStream.readOutbound(); + assertNotNull(encodedHeaders); + encodedHeaders.release(); + + final ChannelInboundHandler controlStreamListener = manager.controlStreamListener(); + controlStreamListener.channelRead(controlStreamHandlerCtx, new DefaultHttp3CancelPushFrame(pushId)); + assertFalse(pushStream.isActive()); + } + + private EmbeddedQuicStreamChannel newPushStream(ChannelHandler pushStreamHandler, long pushId) throws Exception { + return newPushStream(() -> (EmbeddedQuicStreamChannel) manager.newPushStream(pushId, pushStreamHandler).get()); + } + + private EmbeddedQuicStreamChannel newPushStreamWithBootstrap(ChannelHandler pushStreamHandler, long pushId) + throws Exception { + return newPushStream(() -> { + final Promise promise = channel.eventLoop().newPromise(); + manager.newPushStream(pushId, pushStreamHandler, identity(), promise); + return (EmbeddedQuicStreamChannel) promise.get(); + }); + } + + private EmbeddedQuicStreamChannel newPushStream(Callable pushStreamFactory) + throws Exception { + final EmbeddedQuicStreamChannel pushStream = pushStreamFactory.call(); + assertTrue(pushStream.isActive()); + pushStream.flushOutbound(); // flush the stream header + ByteBuf streamHeader = pushStream.readOutbound(); + assertNotNull(streamHeader); + streamHeader.release(); + return pushStream; + } + + private void sendMaxPushId(int maxPushId) throws QpackException { + final DefaultHttp3MaxPushIdFrame maxPushIdFrame = new DefaultHttp3MaxPushIdFrame(maxPushId); + connectionHandler.localControlStreamHandler.channelRead(controlStreamHandlerCtx, maxPushIdFrame); + assertTrue(channel.isActive()); + } + + private static class PushStreamListener extends ChannelOutboundHandlerAdapter { + final List framesWritten = new ArrayList<>(); + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof Http3PushStreamFrame) { + framesWritten.add((Http3PushStreamFrame) msg); + } + super.write(ctx, msg, promise); + } + } + + @FunctionalInterface + private interface PushStreamFactory { + EmbeddedQuicStreamChannel createPushStream(long pushId) throws Exception; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3SpecTestServer.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3SpecTestServer.java new file mode 100644 index 0000000..ab47fb0 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3SpecTestServer.java @@ -0,0 +1,115 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.handler.codec.quic.InsecureQuicTokenHandler; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +public final class Http3SpecTestServer { + private static final byte[] CONTENT = "Hello World!\r\n".getBytes(CharsetUtil.US_ASCII); + static final int PORT = 9999; + + private Http3SpecTestServer() { } + + public static void main(String... args) throws Exception { + int port; + // Allow to pass in the port so we can also use it to run h3spec against + if (args.length == 1) { + port = Integer.parseInt(args[0]); + } else { + port = PORT; + } + NioEventLoopGroup group = new NioEventLoopGroup(1); + SelfSignedCertificate cert = new SelfSignedCertificate(); + QuicSslContext sslContext = QuicSslContextBuilder.forServer(cert.key(), null, cert.cert()) + .applicationProtocols(Http3.supportedApplicationProtocols()).build(); + ChannelHandler codec = Http3.newQuicServerCodecBuilder() + .sslContext(sslContext) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(QuicChannel ch) { + // Called for each connection + ch.pipeline().addLast(new Http3ServerConnectionHandler( + new ChannelInitializer() { + // Called for each request-stream, + @Override + protected void initChannel(QuicStreamChannel ch) { + ch.pipeline().addLast(new Http3RequestStreamInboundHandler() { + + @Override + protected void channelRead( + ChannelHandlerContext ctx, Http3HeadersFrame frame) { + + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelRead( + ChannelHandlerContext ctx, Http3DataFrame frame) { + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) { + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().status("404"); + headersFrame.headers().add("server", "netty"); + headersFrame.headers().addInt("content-length", CONTENT.length); + ctx.write(headersFrame); + ctx.writeAndFlush(new DefaultHttp3DataFrame( + Unpooled.wrappedBuffer(CONTENT))) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + }); + } + }, null, null, null, true)); + } + }).build(); + try { + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(new InetSocketAddress(port)).sync().channel(); + channel.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3TestUtils.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3TestUtils.java new file mode 100644 index 0000000..37f4b13 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3TestUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +final class Http3TestUtils { + + private Http3TestUtils() { } + + static void assertException(Http3ErrorCode code, Throwable e) { + MatcherAssert.assertThat(e, CoreMatchers.instanceOf(Http3Exception.class)); + Http3Exception exception = (Http3Exception) e; + assertEquals(code, exception.errorCode()); + } + + static void verifyClose(int times, Http3ErrorCode expectedCode, EmbeddedQuicChannel channel) { + assertEquals(times, channel.closeErrorCodes().stream().filter(integer -> integer == expectedCode.code).count(), + "Close not invoked with expected times with error code: " + expectedCode.code); + } + + static void verifyClose(Http3ErrorCode expectedCode, EmbeddedQuicChannel channel) { + verifyClose(1, expectedCode, channel); + } + + static void assertBufferEquals(ByteBuf expected, ByteBuf actual) { + try { + assertEquals(expected, actual); + } finally { + ReferenceCountUtil.release(expected); + ReferenceCountUtil.release(actual); + } + } + + static void assertFrameEquals(Http3Frame expected, Http3Frame actual) { + try { + assertEquals(expected, actual); + } finally { + ReferenceCountUtil.release(expected); + ReferenceCountUtil.release(actual); + } + } + + static void assertFrameSame(Http3Frame expected, Http3Frame actual) { + try { + assertSame(expected, actual); + } finally { + // as both frames are the same we only want to release once. + ReferenceCountUtil.release(actual); + } + } + + static void assertFrameReleased(Http3Frame frame) { + if (frame instanceof ReferenceCounted) { + assertEquals(0, ((ReferenceCounted) frame).refCnt()); + } + } + + static Http3Frame newHttp3Frame() { + return () -> 0; + } + + static Http3PushStreamFrame newHttp3PushStreamFrame() { + return () -> 0; + } + + static Http3RequestStreamFrame newHttp3RequestStreamFrame() { + return () -> 0; + } + + static Http3ControlStreamFrame newHttp3ControlStreamFrame() { + return () -> 0; + } + + static DefaultHttp3HeadersFrame newHeadersFrameWithPseudoHeaders() { + final DefaultHttp3HeadersFrame headers = new DefaultHttp3HeadersFrame(); + headers.headers().add(":authority", "netty.quic"); // name only + headers.headers().add(":path", "/"); // name & value + headers.headers().add(":method", "GET"); // name & value with few options per name + headers.headers().add(":scheme", "https"); + return headers; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundHandlerTest.java new file mode 100644 index 0000000..9d14ea1 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/Http3UnidirectionalStreamInboundHandlerTest.java @@ -0,0 +1,283 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.DefaultChannelId; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.function.LongFunction; + +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_CONTROL_STREAM_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_PUSH_STREAM_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_QPACK_DECODER_STREAM_TYPE; +import static io.netty.handler.codec.http3.Http3CodecUtils.HTTP3_QPACK_ENCODER_STREAM_TYPE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http3UnidirectionalStreamInboundHandlerTest { + + private EmbeddedQuicChannel parent; + private Http3ControlStreamOutboundHandler remoteControlStreamHandler; + private Http3ControlStreamInboundHandler localControlStreamHandler; + private QpackEncoder qpackEncoder; + private QpackDecoder qpackDecoder; + + private void setup(boolean server) { + parent = new EmbeddedQuicChannel(server); + qpackEncoder = new QpackEncoder(); + qpackDecoder = new QpackDecoder(0, 0); + localControlStreamHandler = new Http3ControlStreamInboundHandler(server, null, qpackEncoder, + remoteControlStreamHandler); + remoteControlStreamHandler = new Http3ControlStreamOutboundHandler(server, new DefaultHttp3SettingsFrame(), + new CodecHandler()); + } + + @AfterEach + public void tearDown() { + assertFalse(parent.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testUnkownStream(boolean server) throws Exception { + setup(server); + EmbeddedChannel channel = newChannel(server); + ByteBuf buffer = Unpooled.buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, 0x06); + assertFalse(channel.writeInbound(buffer)); + assertEquals(0, buffer.refCnt()); + assertNull(channel.pipeline().context(Http3UnidirectionalStreamInboundHandler.class)); + assertTrue(channel.isActive()); + + // Write some buffer to the stream. This should be just released. + ByteBuf someBuffer = Unpooled.buffer(); + assertFalse(channel.writeInbound(someBuffer)); + assertEquals(0, someBuffer.refCnt()); + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testUnknownStreamWithCustomHandler(boolean server) throws Exception { + setup(server); + long streamType = 0x06; + EmbeddedChannel channel = newChannel(server, v -> { + assertEquals(streamType, v); + // Add an handler that will just forward the received bytes. + return new ChannelInboundHandlerAdapter(); + }); + ByteBuf buffer = Unpooled.buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, streamType); + assertFalse(channel.writeInbound(buffer)); + assertEquals(0, buffer.refCnt()); + assertNull(channel.pipeline().context(Http3UnidirectionalStreamInboundHandler.class)); + assertTrue(channel.isActive()); + + // Write some buffer to the stream. This should be just released. + ByteBuf someBuffer = Unpooled.buffer().writeLong(9); + assertTrue(channel.writeInbound(someBuffer.retainedDuplicate())); + assertTrue(channel.finish()); + + Http3TestUtils.assertBufferEquals(someBuffer, channel.readInbound()); + assertNull(channel.readInbound()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testPushStream(boolean server) throws Exception { + setup(server); + EmbeddedChannel channel = newChannel(server); + ByteBuf buffer = Unpooled.buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, HTTP3_PUSH_STREAM_TYPE); + Http3CodecUtils.writeVariableLengthInteger(buffer, 2); + assertFalse(channel.writeInbound(buffer)); + assertEquals(0, buffer.refCnt()); + if (server) { + Http3TestUtils.verifyClose(Http3ErrorCode.H3_STREAM_CREATION_ERROR, (EmbeddedQuicChannel) channel.parent()); + } else { + ByteBuf b = Unpooled.buffer(); + assertFalse(channel.writeInbound(b)); + assertEquals(0, b.refCnt()); + } + assertFalse(channel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testPushStreamNoMaxPushIdFrameSent(boolean server) throws Exception { + testPushStream(server, -1); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testPushStreamMaxPushIdFrameSentWithSmallerId(boolean server) throws Exception { + testPushStream(server, 0); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testPushStreamMaxPushIdFrameSentWithSameId(boolean server) throws Exception { + testPushStream(server, 2); + } + + private void testPushStream(boolean server, long maxPushId) throws Exception { + setup(server); + assertFalse(parent.finish()); + parent = new EmbeddedQuicChannel(server, server ? + new Http3ServerConnectionHandler(new ChannelInboundHandlerAdapter()) : + new Http3ClientConnectionHandler()); + final EmbeddedQuicStreamChannel localControlStream = + (EmbeddedQuicStreamChannel) Http3.getLocalControlStream(parent); + assertNotNull(localControlStream); + assertTrue(localControlStream.releaseOutbound()); + EmbeddedQuicStreamChannel outboundControlChannel = + (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.BIDIRECTIONAL, + remoteControlStreamHandler).get(); + + // Let's drain everything that was written while channelActive(...) was called. + for (;;) { + Object written = outboundControlChannel.readOutbound(); + if (written == null) { + break; + } + ReferenceCountUtil.release(written); + } + + if (maxPushId >= 0) { + assertTrue(outboundControlChannel.writeOutbound(new DefaultHttp3MaxPushIdFrame(maxPushId))); + Object push = outboundControlChannel.readOutbound(); + ReferenceCountUtil.release(push); + } + + Http3UnidirectionalStreamInboundHandler handler = newUniStreamInboundHandler(server, null); + EmbeddedQuicStreamChannel channel = + (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, handler).get(); + + ByteBuf buffer = Unpooled.buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, HTTP3_PUSH_STREAM_TYPE); + final int pushId = 2; + Http3CodecUtils.writeVariableLengthInteger(buffer, pushId); + + assertFalse(channel.writeInbound(buffer)); + assertEquals(0, buffer.refCnt()); + if (server) { + Http3TestUtils.verifyClose(Http3ErrorCode.H3_STREAM_CREATION_ERROR, parent); + } else { + if (pushId > maxPushId) { + Http3TestUtils.verifyClose(Http3ErrorCode.H3_ID_ERROR, parent); + } else { + assertTrue(parent.isActive()); + assertNotNull(channel.pipeline().context(CodecHandler.class)); + } + } + assertFalse(channel.finish()); + assertFalse(outboundControlChannel.finish()); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testControlStream(boolean server) throws Exception { + testStreamSetup(server, HTTP3_CONTROL_STREAM_TYPE, Http3ControlStreamInboundHandler.class, true); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testQpackEncoderStream(boolean server) throws Exception { + testStreamSetup(server, HTTP3_QPACK_ENCODER_STREAM_TYPE, QpackEncoderHandler.class, false); + } + + @ParameterizedTest(name = "{index}: server = {0}") + @ValueSource(booleans = {false, true}) + public void testQpackDecoderStream(boolean server) throws Exception { + testStreamSetup(server, HTTP3_QPACK_DECODER_STREAM_TYPE, QpackDecoderHandler.class, false); + } + + private void testStreamSetup(boolean server, long type, + Class clazz, boolean hasCodec) throws Exception { + setup(server); + EmbeddedChannel channel = newChannel(server); + ByteBuf buffer = Unpooled.buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, type); + assertFalse(channel.writeInbound(buffer)); + assertEquals(0, buffer.refCnt()); + assertNull(channel.pipeline().context(Http3UnidirectionalStreamInboundHandler.class)); + assertNotNull(channel.pipeline().context(clazz)); + if (hasCodec) { + assertNotNull(channel.pipeline().context(CodecHandler.class)); + } else { + assertNull(channel.pipeline().context(CodecHandler.class)); + } + assertFalse(channel.finish()); + + channel = new EmbeddedChannel(channel.parent(), DefaultChannelId.newInstance(), + true, false, newUniStreamInboundHandler(server, null)); + + // Try to create the stream a second time, this should fail + buffer = Unpooled.buffer(8); + Http3CodecUtils.writeVariableLengthInteger(buffer, type); + assertFalse(channel.writeInbound(buffer)); + assertEquals(0, buffer.refCnt()); + Http3TestUtils.verifyClose(Http3ErrorCode.H3_STREAM_CREATION_ERROR, (EmbeddedQuicChannel) channel.parent()); + assertFalse(channel.finish()); + } + + private EmbeddedChannel newChannel(boolean server) throws Exception { + return newChannel(server, null); + } + + private EmbeddedChannel newChannel(boolean server, LongFunction unknownStreamHandlerFactory) + throws Exception { + Http3UnidirectionalStreamInboundHandler handler = + newUniStreamInboundHandler(server, unknownStreamHandlerFactory); + return (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.BIDIRECTIONAL, handler).get(); + } + + private Http3UnidirectionalStreamInboundHandler newUniStreamInboundHandler(boolean server, + LongFunction unknownStreamHandlerFactory) { + return server ? + new Http3UnidirectionalStreamInboundServerHandler((v, __, ___) -> new CodecHandler(), + localControlStreamHandler, remoteControlStreamHandler, unknownStreamHandlerFactory, + () -> new QpackEncoderHandler((long) Integer.MAX_VALUE, qpackDecoder), + () -> new QpackDecoderHandler(qpackEncoder)) : + new Http3UnidirectionalStreamInboundClientHandler((v, __, ___) -> new CodecHandler(), + localControlStreamHandler, remoteControlStreamHandler, + unknownStreamHandlerFactory, + pushId -> new Http3PushStreamClientInitializer() { + @Override + protected void initPushStream(QuicStreamChannel ch) { + ch.pipeline().addLast(new CodecHandler()); + } + }, + () -> new QpackEncoderHandler((long) Integer.MAX_VALUE, + qpackDecoder), () -> new QpackDecoderHandler(qpackEncoder)); + } + + private static final class CodecHandler extends ChannelHandlerAdapter { } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/HttpConversionUtilTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/HttpConversionUtilTest.java new file mode 100644 index 0000000..d377abe --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/HttpConversionUtilTest.java @@ -0,0 +1,228 @@ +/* + * 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.handler.codec.http3; + +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.AsciiString; +import org.junit.jupiter.api.Test; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE; +import static io.netty.handler.codec.http.HttpHeaderNames.HOST; +import static io.netty.handler.codec.http.HttpHeaderNames.KEEP_ALIVE; +import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.TE; +import static io.netty.handler.codec.http.HttpHeaderNames.TRANSFER_ENCODING; +import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE; +import static io.netty.handler.codec.http.HttpHeaderValues.GZIP; +import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HttpConversionUtilTest { + + @Test + public void connectNoPath() throws Exception { + String authority = "netty.io:80"; + Http3Headers headers = new DefaultHttp3Headers(); + headers.authority(authority); + headers.method(HttpMethod.CONNECT.asciiName()); + HttpRequest request = HttpConversionUtil.toHttpRequest(0, headers, true); + assertNotNull(request); + assertEquals(authority, request.uri()); + assertEquals(authority, request.headers().get(HOST)); + } + + @Test + public void setHttp3AuthorityWithoutUserInfo() { + Http3Headers headers = new DefaultHttp3Headers(); + + HttpConversionUtil.setHttp3Authority("foo", headers); + assertEquals(new AsciiString("foo"), headers.authority()); + } + + @Test + public void setHttp3AuthorityWithUserInfo() { + Http3Headers headers = new DefaultHttp3Headers(); + + HttpConversionUtil.setHttp3Authority("info@foo", headers); + assertEquals(new AsciiString("foo"), headers.authority()); + + HttpConversionUtil.setHttp3Authority("@foo.bar", headers); + assertEquals(new AsciiString("foo.bar"), headers.authority()); + } + + @Test + public void setHttp3AuthorityNullOrEmpty() { + Http3Headers headers = new DefaultHttp3Headers(); + + HttpConversionUtil.setHttp3Authority(null, headers); + assertNull(headers.authority()); + + HttpConversionUtil.setHttp3Authority("", headers); + assertSame(AsciiString.EMPTY_STRING, headers.authority()); + } + + @Test + public void setHttp2AuthorityWithEmptyAuthority() { + assertThrows(IllegalArgumentException.class, + () -> HttpConversionUtil.setHttp3Authority("info@", new DefaultHttp3Headers())); + } + + @Test + public void stripTEHeaders() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(TE, GZIP); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertTrue(out.isEmpty()); + } + + @Test + public void stripTEHeadersExcludingTrailers() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(TE, GZIP); + inHeaders.add(TE, TRAILERS); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertSame(TRAILERS, out.get(TE)); + } + + @Test + public void stripTEHeadersCsvSeparatedExcludingTrailers() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(TE, GZIP + "," + TRAILERS); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertSame(TRAILERS, out.get(TE)); + } + + @Test + public void stripTEHeadersCsvSeparatedAccountsForValueSimilarToTrailers() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(TE, GZIP + "," + TRAILERS + "foo"); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertFalse(out.contains(TE)); + } + + @Test + public void stripTEHeadersAccountsForValueSimilarToTrailers() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(TE, TRAILERS + "foo"); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertFalse(out.contains(TE)); + } + + @Test + public void stripTEHeadersAccountsForOWS() { + HttpHeaders inHeaders = new DefaultHttpHeaders(false); + inHeaders.add(TE, " " + TRAILERS + " "); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertSame(TRAILERS, out.get(TE)); + } + + @Test + public void stripConnectionHeadersAndNominees() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(CONNECTION, "foo"); + inHeaders.add("foo", "bar"); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertTrue(out.isEmpty()); + } + + @Test + public void stripConnectionNomineesWithCsv() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(CONNECTION, "foo, bar"); + inHeaders.add("foo", "baz"); + inHeaders.add("bar", "qux"); + inHeaders.add("hello", "world"); + Http3Headers out = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, out); + assertEquals(1, out.size()); + assertSame("world", out.get("hello")); + } + + @Test + public void addHttp3ToHttpHeadersCombinesCookies() throws Http3Exception { + Http3Headers inHeaders = new DefaultHttp3Headers(); + inHeaders.add("yes", "no"); + inHeaders.add(COOKIE, "foo=bar"); + inHeaders.add(COOKIE, "bax=baz"); + + HttpHeaders outHeaders = new DefaultHttpHeaders(); + + HttpConversionUtil.addHttp3ToHttpHeaders(5, inHeaders, outHeaders, HttpVersion.HTTP_1_1, false, false); + assertEquals("no", outHeaders.get("yes")); + assertEquals("foo=bar; bax=baz", outHeaders.get(COOKIE.toString())); + } + + @Test + public void connectionSpecificHeadersShouldBeRemoved() { + HttpHeaders inHeaders = new DefaultHttpHeaders(); + inHeaders.add(CONNECTION, "keep-alive"); + inHeaders.add(HOST, "example.com"); + @SuppressWarnings("deprecation") + AsciiString keepAlive = KEEP_ALIVE; + inHeaders.add(keepAlive, "timeout=5, max=1000"); + @SuppressWarnings("deprecation") + AsciiString proxyConnection = PROXY_CONNECTION; + inHeaders.add(proxyConnection, "timeout=5, max=1000"); + inHeaders.add(TRANSFER_ENCODING, "chunked"); + inHeaders.add(UPGRADE, "h2c"); + + Http3Headers outHeaders = new DefaultHttp3Headers(); + HttpConversionUtil.toHttp3Headers(inHeaders, outHeaders); + + assertFalse(outHeaders.contains(CONNECTION)); + assertFalse(outHeaders.contains(HOST)); + assertFalse(outHeaders.contains(keepAlive)); + assertFalse(outHeaders.contains(proxyConnection)); + assertFalse(outHeaders.contains(TRANSFER_ENCODING)); + assertFalse(outHeaders.contains(UPGRADE)); + } + + @Test + public void http3ToHttpHeaderTest() throws Exception { + Http3Headers http3Headers = new DefaultHttp3Headers(); + http3Headers.status("200"); + http3Headers.path("/meow"); // HTTP/2 Header response should not contain 'path' in response. + http3Headers.set("cat", "meow"); + + HttpHeaders httpHeaders = new DefaultHttpHeaders(); + HttpConversionUtil.addHttp3ToHttpHeaders(3, http3Headers, httpHeaders, HttpVersion.HTTP_1_1, false, true); + assertFalse(httpHeaders.contains(HttpConversionUtil.ExtensionHeaderNames.PATH.text())); + assertEquals("meow", httpHeaders.get("cat")); + + httpHeaders.clear(); + HttpConversionUtil.addHttp3ToHttpHeaders(3, http3Headers, httpHeaders, HttpVersion.HTTP_1_1, false, false); + assertTrue(httpHeaders.contains(HttpConversionUtil.ExtensionHeaderNames.PATH.text())); + assertEquals("meow", httpHeaders.get("cat")); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderDynamicTableTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderDynamicTableTest.java new file mode 100644 index 0000000..ebd0473 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderDynamicTableTest.java @@ -0,0 +1,129 @@ +/* + * 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.handler.codec.http3; + + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QpackDecoderDynamicTableTest { + + private final QpackHeaderField fooBar = new QpackHeaderField("foo", "bar"); + + @Test + public void length() throws Exception { + QpackDecoderDynamicTable table = newTable(100); + assertEquals(0, table.length()); + table.add(fooBar); + assertEquals(1, table.length()); + table.clear(); + assertEquals(0, table.length()); + } + + @Test + public void size() throws Exception { + QpackDecoderDynamicTable table = newTable(100); + assertEquals(0, table.size()); + QpackHeaderField entry = new QpackHeaderField("foo", "bar"); + table.add(entry); + assertEquals(entry.size(), table.size()); + table.clear(); + assertEquals(0, table.size()); + } + + @Test + public void getEntry() throws Exception { + QpackDecoderDynamicTable table = newTable(100); + QpackHeaderField entry = new QpackHeaderField("foo", "bar"); + table.add(entry); + assertEquals(entry, table.getEntry(0)); + table.clear(); + + assertThrows(QpackException.class, () -> table.getEntry(0)); + } + + @Test + public void getEntryExceptionally() throws Exception { + QpackDecoderDynamicTable table = newTable(1); + + assertThrows(QpackException.class, () -> table.getEntry(0)); + } + + @Test + public void setCapacity() throws Exception { + QpackHeaderField entry1 = new QpackHeaderField("foo", "bar"); + QpackHeaderField entry2 = new QpackHeaderField("hello", "world"); + final long size1 = entry1.size(); + final long size2 = entry2.size(); + QpackDecoderDynamicTable table = newTable(size1 + size2); + table.add(entry1); + table.add(entry2); + assertEquals(2, table.length()); + assertEquals(size1 + size2, table.size()); + assertEquals(entry1, table.getEntry(0)); + assertEquals(entry2, table.getEntry(1)); + + table.setCapacity((size1 + size2) * 2); //larger capacity + assertEquals(2, table.length()); + assertEquals(size1 + size2, table.size()); + + table.setCapacity(size2); //smaller capacity + //entry1 will be removed + assertEquals(1, table.length()); + assertEquals(size2, table.size()); + assertEquals(entry2, table.getEntry(0)); + table.setCapacity(0); //clear all + assertEquals(0, table.length()); + assertEquals(0, table.size()); + } + + @Test + public void add() throws Exception { + QpackDecoderDynamicTable table = newTable(100); + assertEquals(0, table.size()); + QpackHeaderField entry1 = new QpackHeaderField("foo", "bar"); //size:3+3+32=38 + QpackHeaderField entry2 = new QpackHeaderField("hello", "world"); + table.add(entry1); //success + assertEquals(entry1.size(), table.size()); + assertEquals(entry1, table.getEntry(0)); + table.setCapacity(32); //entry1 is removed from table + assertEquals(0, table.size()); + assertEquals(0, table.length()); + + table.setCapacity(64); + table.add(entry1); //success + assertEquals(entry1.size(), table.size()); + assertEquals(1, table.length()); + assertEquals(entry1, table.getEntry(0)); + table.add(entry2); //entry2 is added, but entry1 is removed from table + assertEquals(entry2.size(), table.size()); + assertEquals(1, table.length()); + assertEquals(entry2, table.getEntry(1)); + + table.setCapacity(128); + table.add(entry1); //success + assertEquals(entry2, table.getEntry(0)); + } + + private static QpackDecoderDynamicTable newTable(long capacity) throws QpackException { + QpackDecoderDynamicTable table = new QpackDecoderDynamicTable(); + table.setCapacity(capacity); + return table; + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderHandlerTest.java new file mode 100644 index 0000000..720327a --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderHandlerTest.java @@ -0,0 +1,359 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.quic.QuicStreamType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.function.Consumer; + +import static io.netty.handler.codec.http3.Http3.setQpackAttributes; +import static io.netty.handler.codec.http3.Http3ErrorCode.QPACK_DECODER_STREAM_ERROR; +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY; +import static io.netty.handler.codec.http3.QpackUtil.encodePrefixedInteger; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QpackDecoderHandlerTest { + private static final QpackHeaderField fooBar = new QpackHeaderField("foo", "bar"); + private final QpackEncoderDynamicTable dynamicTable = new QpackEncoderDynamicTable(); + private EmbeddedQuicChannel parent; + private QpackEncoder encoder; + private EmbeddedQuicStreamChannel decoderStream; + private EmbeddedQuicStreamChannel encoderStream; + private int maxEntries; + private QpackAttributes attributes; + + @AfterEach + public void tearDown() { + assertFalse(encoderStream.finish()); + assertFalse(decoderStream.finish()); + } + + @Test + public void sectionAckNoIncrement() throws Exception { + setup(128L); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + + Http3Exception e = assertThrows(Http3Exception.class, () -> sendAckForStreamId(decoderStream.streamId())); + assertThat(e.getCause(), instanceOf(QpackException.class)); + + Http3TestUtils.verifyClose(QPACK_DECODER_STREAM_ERROR, parent); + finishStreams(); + } + + @Test + public void sectionAck() throws Exception { + setup(128L); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + verifyRequiredInsertCount(1); + sendInsertCountIncrement(1); + verifyKnownReceivedCount(1); + + // Refer now to dynamic table + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendAckForStreamId(decoderStream.streamId()); + + finishStreams(); + + verifyRequiredInsertCount(1); + verifyKnownReceivedCount(1); + } + + @Test + public void sectionAckUnknownStream() throws Exception { + setup(128); + + Http3Exception e = assertThrows(Http3Exception.class, () -> sendAckForStreamId(1)); + assertThat(e.getCause(), instanceOf(QpackException.class)); + + Http3TestUtils.verifyClose(QPACK_DECODER_STREAM_ERROR, parent); + finishStreams(); + } + + @Test + public void sectionAckAlreadyAcked() throws Exception { + setup(128); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendInsertCountIncrement(1); + // Refer now to dynamic table + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendAckForStreamId(decoderStream.streamId()); + + Http3Exception e = assertThrows(Http3Exception.class, () -> sendAckForStreamId(decoderStream.streamId())); + assertThat(e.getCause(), instanceOf(QpackException.class)); + + Http3TestUtils.verifyClose(QPACK_DECODER_STREAM_ERROR, parent); + finishStreams(); + + verifyRequiredInsertCount(1); + verifyKnownReceivedCount(1); + } + + @Test + public void sectionAckMultiPending() throws Exception { + setup(128L); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendInsertCountIncrement(1); + // Refer now to dynamic table + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + + sendAckForStreamId(decoderStream.streamId()); + sendAckForStreamId(decoderStream.streamId()); + + finishStreams(); + + verifyRequiredInsertCount(1); + verifyKnownReceivedCount(1); + } + + @Test + public void sectionAckMultiPostAck() throws Exception { + setup(128L); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendInsertCountIncrement(1); + // Refer now to dynamic table + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendAckForStreamId(decoderStream.streamId()); + + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendAckForStreamId(decoderStream.streamId()); + + finishStreams(); + + verifyRequiredInsertCount(1); + verifyKnownReceivedCount(1); + } + + @Test + public void sectionAckCancelledStream() throws Exception { + setup(128L); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendInsertCountIncrement(1); + // Refer now to dynamic table + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + + sendStreamCancellation(decoderStream.streamId()); + + Http3Exception e = assertThrows(Http3Exception.class, () -> sendAckForStreamId(decoderStream.streamId())); + assertThat(e.getCause(), instanceOf(QpackException.class)); + + Http3TestUtils.verifyClose(QPACK_DECODER_STREAM_ERROR, parent); + finishStreams(); + } + + @Test + public void splitBufferForSectionAck() throws Exception { + setup(128); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + verifyRequiredInsertCount(1); + sendInsertCountIncrement(1); + verifyKnownReceivedCount(1); + + // Refer now to dynamic table + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + final ByteBuf buf = encodeSectionAck(decoderStream.streamId()); + try { + while (buf.isReadable()) { + assertFalse(decoderStream.writeInbound(buf.readBytes(1))); + } + } finally { + buf.release(); + } + finishStreams(); + } + + @Test + public void splitBufferForInsertCountIncrement() throws Exception { + setup(128); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + verifyRequiredInsertCount(1); + final ByteBuf buf = encodeInsertCountIncrement(1); + try { + while (buf.isReadable()) { + assertFalse(decoderStream.writeInbound(buf.readBytes(1))); + } + } finally { + buf.release(); + } + verifyKnownReceivedCount(1); + finishStreams(); + } + + @Test + public void splitBufferForStreamCancellation() throws Exception { + setup(128); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + verifyRequiredInsertCount(1); + final ByteBuf buf = encodeStreamCancellation(decoderStream.streamId()); + try { + while (buf.isReadable()) { + assertFalse(decoderStream.writeInbound(buf.readBytes(1))); + } + } finally { + buf.release(); + } + finishStreams(); + } + + @Test + public void streamCancel() throws Exception { + setup(128); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + verifyRequiredInsertCount(1); + sendInsertCountIncrement(1); + verifyKnownReceivedCount(1); + + // Refer now to dynamic table + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + sendStreamCancellation(decoderStream.streamId()); + verifyRequiredInsertCount(1); + verifyKnownReceivedCount(1); + finishStreams(); + } + + @Test + public void streamCancelUnknownStream() throws Exception { + setup(128); + sendStreamCancellation(decoderStream.streamId()); + verifyRequiredInsertCount(0); + verifyKnownReceivedCount(0); + finishStreams(); + } + + @Test + public void streamCancelDynamicTableWithMaxCapacity0() throws Exception { + setup(0); + encodeHeaders(headers -> headers.add(fooBar.name, fooBar.value)); + verifyRequiredInsertCount(0); + verifyKnownReceivedCount(0); + // Send a stream cancellation for a dynamic table of capacity 0. + // See https://www.rfc-editor.org/rfc/rfc9204.html#section-2.2.2.2 + sendStreamCancellation(decoderStream.streamId()); + finishStreams(false); + } + + @Test + public void invalidIncrement() throws Exception { + setup(128); + Http3Exception e = assertThrows(Http3Exception.class, () -> sendInsertCountIncrement(2)); + assertThat(e.getCause(), instanceOf(QpackException.class)); + + Http3TestUtils.verifyClose(QPACK_DECODER_STREAM_ERROR, parent); + finishStreams(); + } + + private void sendAckForStreamId(long streamId) throws Http3Exception { + assertFalse(decoderStream.writeInbound(encodeSectionAck(streamId))); + } + + private ByteBuf encodeSectionAck(long streamId) { + final ByteBuf ack = decoderStream.alloc().buffer(); + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-section-acknowledgment + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Stream ID (7+) | + // +---+---------------------------+ + encodePrefixedInteger(ack, (byte) 0b1000_0000, 7, streamId); + return ack; + } + + private void sendInsertCountIncrement(long increment) throws Http3Exception { + assertFalse(decoderStream.writeInbound(encodeInsertCountIncrement(increment))); + } + + private ByteBuf encodeInsertCountIncrement(long increment) { + final ByteBuf incr = decoderStream.alloc().buffer(); + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-insert-count-increment + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | Increment (6+) | + // +---+---+-----------------------+ + encodePrefixedInteger(incr, (byte) 0b0000_0000, 6, increment); + return incr; + } + + private void sendStreamCancellation(long streamId) { + assertFalse(decoderStream.writeInbound(encodeStreamCancellation(streamId))); + } + + private ByteBuf encodeStreamCancellation(long streamId) { + final ByteBuf incr = decoderStream.alloc().buffer(); + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-stream-cancellation + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Stream ID (6+) | + // +---+---+-----------------------+ + encodePrefixedInteger(incr, (byte) 0b0100_0000, 6, streamId); + return incr; + } + + private void encodeHeaders(Consumer headersUpdater) { + Http3Headers headers = new DefaultHttp3Headers(); + headersUpdater.accept(headers); + final ByteBuf buf = decoderStream.alloc().buffer(); + try { + encoder.encodeHeaders(attributes, buf, decoderStream.alloc(), decoderStream.streamId(), headers); + } finally { + buf.release(); + } + } + + private void setup(long maxTableCapacity) throws Exception { + maxEntries = Math.toIntExact(QpackUtil.maxEntries(maxTableCapacity)); + parent = new EmbeddedQuicChannel(true); + attributes = new QpackAttributes(parent, false); + setQpackAttributes(parent, attributes); + Http3SettingsFrame settings = new DefaultHttp3SettingsFrame(); + settings.put(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, maxTableCapacity); + QpackDecoder decoder = new QpackDecoder(maxTableCapacity, 0); + encoderStream = (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, + new QpackEncoderHandler(maxTableCapacity, decoder)).get(); + attributes.encoderStream(encoderStream); + encoder = new QpackEncoder(dynamicTable); + encoder.configureDynamicTable(attributes, maxTableCapacity, 0); + decoderStream = (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, + new QpackDecoderHandler(encoder)).get(); + attributes.decoderStream(decoderStream); + } + + private void finishStreams() { + finishStreams(true); + } + + private void finishStreams(boolean encoderPendingMessage) { + assertThat("Unexpected decoder stream message", decoderStream.finishAndReleaseAll(), is(false)); + assertThat("Unexpected encoder stream message", encoderStream.finishAndReleaseAll(), is(encoderPendingMessage)); + assertThat("Unexpected parent stream message", parent.finishAndReleaseAll(), is(false)); + } + + private void verifyRequiredInsertCount(int insertCount) { + assertThat("Unexpected dynamic table insert count.", + dynamicTable.encodedRequiredInsertCount(dynamicTable.insertCount()), + is(insertCount == 0 ? 0 : insertCount % maxEntries + 1)); + } + + private void verifyKnownReceivedCount(int receivedCount) { + assertThat("Unexpected dynamic table known received count.", dynamicTable.encodedKnownReceivedCount(), + is(receivedCount == 0 ? 0 : receivedCount % maxEntries + 1)); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderTest.java new file mode 100644 index 0000000..19a1d86 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackDecoderTest.java @@ -0,0 +1,169 @@ +/* + * 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collection; + +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY; +import static io.netty.handler.codec.http3.QpackDecoderStateSyncStrategy.ackEachInsert; +import static io.netty.handler.codec.http3.QpackUtil.MAX_UNSIGNED_INT; +import static java.lang.Math.toIntExact; +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class QpackDecoderTest { + private static final String FOO = "foo"; + private static final String BAR = "bar"; + private QpackDecoderDynamicTable table; + private EmbeddedQuicStreamChannel decoderStream; + + private QpackDecoder decoder; + private int inserted; + private int maxEntries; + private QpackAttributes attributes; + + public static Collection data() { + int capacity = 128; // maxEntries = 128/32 = 4, maxIndex = 2*4 = 8 + return asList( + new Object[]{capacity, 0}, + new Object[]{capacity, 1}, + new Object[]{capacity, 5}, + new Object[]{capacity, 8}, + new Object[]{capacity, 16}, + new Object[]{capacity, 25}, + new Object[]{capacity, 64}, + new Object[]{capacity, 89} + ); + } + + @ParameterizedTest(name = "capacity: {0}, inserts: {1}") + @MethodSource("data") + public void requiredInsertCountAsInserted(int capacity, int insertionCount) throws Exception { + setup(capacity); + + insertLiterals(insertionCount); + encodeDecodeVerifyRequiredInsertCount(inserted); + } + + @ParameterizedTest(name = "capacity: {0}, inserts: {1}") + @MethodSource("data") + public void requiredInsertCountLessThanInserted(int capacity, int insertionCount) throws Exception { + setup(capacity); + assumeTrue(insertionCount > 0); + + insertLiterals(insertionCount); + encodeDecodeVerifyRequiredInsertCount(insertionCount - 1); + } + + @ParameterizedTest(name = "capacity: {0}, inserts: {1}") + @MethodSource("data") + public void requiredInsertCountBehindMax(int capacity, int insertionCount) throws Exception { + setup(capacity); + assumeTrue(insertionCount > maxEntries); + + insertLiterals(insertionCount); + encodeDecodeVerifyRequiredInsertCount(insertionCount - maxEntries + 1); + } + + @ParameterizedTest(name = "capacity: {0}, inserts: {1}") + @MethodSource("data") + public void getWithRelativeIndex(int capacity, int insertionCount) throws Exception { + setup(capacity); + assumeTrue(insertionCount > 3); + + insertLiterals(insertionCount); + int requiredInsertCount = encodeDecodeRequiredInsertCount(insertionCount); + int base = encodeDecodeDeltaBase(requiredInsertCount, false, 1); + int relativeIndex = 1; + final QpackHeaderField entry = table.getEntryRelativeEncodedField(base - relativeIndex); + verifyField(entry, insertionCount - 2); + } + + @ParameterizedTest(name = "capacity: {0}, inserts: {1}") + @MethodSource("data") + public void getWithPostBaseRelativeIndex(int capacity, int insertionCount) throws Exception { + setup(capacity); + assumeTrue(insertionCount > 2); + + insertLiterals(insertionCount); + int requiredInsertCount = encodeDecodeRequiredInsertCount(insertionCount - 1); + int base = encodeDecodeDeltaBase(requiredInsertCount, true, 0); + int relativeIndex = 1; + final QpackHeaderField entry = table.getEntryRelativeEncodedField(base - relativeIndex); + verifyField(entry, insertionCount - 1); + } + + private void setup(long capacity) throws QpackException { + long maxTableCapacity = MAX_UNSIGNED_INT; + inserted = 0; + this.maxEntries = toIntExact(QpackUtil.maxEntries(maxTableCapacity)); + final DefaultHttp3SettingsFrame settings = new DefaultHttp3SettingsFrame(); + settings.put(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, maxTableCapacity); + table = new QpackDecoderDynamicTable(); + EmbeddedQuicChannel parent = new EmbeddedQuicChannel(true); + attributes = new QpackAttributes(parent, false); + decoderStream = new EmbeddedQuicStreamChannel(); + attributes.decoderStream(decoderStream); + decoder = new QpackDecoder(maxTableCapacity, 0, table, ackEachInsert()); + decoder.setDynamicTableCapacity(capacity); + } + + private void encodeDecodeVerifyRequiredInsertCount(int count) throws QpackException { + final int ric = encodeDecodeRequiredInsertCount(count); + assertThat(ric, is(count)); + } + + private int encodeDecodeDeltaBase(int requiredInsertCount, boolean postBase, int deltaBase) throws QpackException { + final ByteBuf buf = Unpooled.buffer(); + QpackUtil.encodePrefixedInteger(buf, (byte) (postBase ? 0b0 : 0b1000_0000), 8, deltaBase); + try { + return decoder.decodeBase(buf, requiredInsertCount); + } finally { + buf.release(); + } + } + + private int encodeDecodeRequiredInsertCount(int count) throws QpackException { + final ByteBuf buf = Unpooled.buffer(); + QpackUtil.encodePrefixedInteger(buf, (byte) 0b0, 8, count == 0 ? 0 : count % (2L * maxEntries) + 1); + try { + return decoder.decodeRequiredInsertCount(attributes, buf); + } finally { + buf.release(); + } + } + + private void insertLiterals(int count) throws QpackException { + for (int i = 1; i <= count; i++) { + inserted++; + decoder.insertLiteral(decoderStream, FOO + i, BAR + i); + } + assertThat(decoderStream.finishAndReleaseAll(), is(count > 0)); + } + + private void verifyField(QpackHeaderField field, int fieldIndexWhenInserted) { + assertThat(field.name, is(FOO + fieldIndexWhenInserted)); + assertThat(field.value, is(BAR + fieldIndexWhenInserted)); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackEncoderDecoderTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackEncoderDecoderTest.java new file mode 100644 index 0000000..c3d052f --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackEncoderDecoderTest.java @@ -0,0 +1,541 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.util.AsciiString; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.LinkedBlockingQueue; + +import static io.netty.buffer.UnpooledByteBufAllocator.DEFAULT; +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS; +import static io.netty.handler.codec.http3.Http3SettingsFrame.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY; +import static io.netty.handler.codec.quic.QuicStreamType.UNIDIRECTIONAL; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class QpackEncoderDecoderTest { + + private QpackEncoder encoder; + private QpackDecoder decoder; + private boolean stateSyncStrategyAckNextInsert = true; + private int headersAdded; + private int maxEntries; + private QpackEncoderDynamicTable encDynamicTable; + private QpackDecoderDynamicTable decDynamicTable; + private BlockingQueue> suspendedEncoderInstructions; + + private final QpackDecoderStateSyncStrategy syncStrategy = mock(QpackDecoderStateSyncStrategy.class); + private final Http3Headers encHeaders = new DefaultHttp3Headers(); + private final Http3Headers decHeaders = new DefaultHttp3Headers(); + private final ByteBuf out = Unpooled.buffer(); + private final EmbeddedQuicChannel parent = new EmbeddedQuicChannel(true); + private QpackAttributes attributes; + + @AfterEach + public void tearDown() { + out.release(); + } + + @Test + public void dynamicIndexed() throws Exception { + setup(128, 0); + headersAdded++; + testDynamicTableIndexed("foo", "bar"); + } + + @Test + public void dynamicIndexedWithBlockedStreams() throws Exception { + setup(128, 100); + headersAdded++; + testDynamicTableIndexedWithBlockedStreams("foo", "bar"); + } + + @Test + public void dynamicIndexedWithStaticTableNameRef() throws Exception { + setup(128, 0); + headersAdded++; + testDynamicTableIndexed(":authority", "netty.quic"); + } + + @Test + public void dynamicIndexedWithStaticTableNameRefWithBlockedStreams() throws Exception { + setup(128, 100); + headersAdded++; + testDynamicTableIndexedWithBlockedStreams(":authority", "netty.quic"); + } + + @Test + public void dynamicIndexedWithNameRef() throws Exception { + setup(128, 0); + + headersAdded++; + testDynamicTableIndexed("foo", "bar"); + + resetState(); + + headersAdded++; + testDynamicTableIndexed("foo", "bar2"); + + resetState(); + + testDynamicTableIndexed("foo", "bar"); + } + + @Test + public void dynamicIndexedWithNameRefWithBlockedStream() throws Exception { + setup(128, 100); + + headersAdded++; + testDynamicTableIndexedWithBlockedStreams("foo", "bar"); + + resetState(); + + headersAdded++; + testDynamicTableIndexedWithBlockedStreams("foo", "bar2"); + } + + @Test + public void indexWrapAround() throws Exception { + setup(128, 0); // maxEntries => 128/32 = 4, full range = 2*4 = 8 + + addEncodeHeader("foo", "bar", 3); + encode(out, encHeaders); + verifyRequiredInsertCount(3); + verifyKnownReceivedCount(3); + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(3)); + assertThat(decHeaders.size(), is(3)); + verifyDecodedHeaders("foo", "bar", 3); + + resetState(); + addEncodeHeader("boo", "far", 3); + encode(out, encHeaders); + verifyRequiredInsertCount(6); + verifyKnownReceivedCount(6); + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(6)); + assertThat(decHeaders.size(), is(3)); + verifyDecodedHeaders("boo", "far", 3); + + resetState(); + addEncodeHeader("zoo", "gar", 3); + encode(out, encHeaders); + verifyRequiredInsertCount(9); + verifyKnownReceivedCount(9); + + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(9)); + assertThat(decHeaders.size(), is(3)); + verifyDecodedHeaders("zoo", "gar", 3); + + // Now reuse the headers for encode to use dynamic table. + resetState(); + assertThat("Header not found in encoder dynamic table.", + encDynamicTable.getEntryIndex("zoo1", "gar"), greaterThanOrEqualTo(0)); + assertThat("Header not found in encoder dynamic table.", + encDynamicTable.getEntryIndex("zoo2", "gar"), greaterThanOrEqualTo(0)); + encHeaders.add("zoo1", "gar"); + encHeaders.add("zoo2", "gar"); + encode(out, encHeaders); + verifyRequiredInsertCount(9); // No new inserts + verifyKnownReceivedCount(9); // No new inserts + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(9)); + assertThat(decHeaders.size(), is(2)); + verifyDecodedHeader("zoo1", "gar"); + verifyDecodedHeader("zoo2", "gar"); + } + + @Test + public void indexWrapAroundWithBlockedStreams() throws Exception { + setup(128, 100); // maxEntries => 128/32 = 4, full range = 2*4 = 8 + + addEncodeHeader("foo", "bar", 3); + encode(out, encHeaders); + verifyRequiredInsertCount(3); + verifyKnownReceivedCount(0); + assertThat(decDynamicTable.insertCount(), is(0)); + + drainNextSuspendedEncoderInstruction(); + decode(out, decHeaders); + drainAllSuspendedEncoderInstructions(); + assertThat(decDynamicTable.insertCount(), is(3)); + verifyKnownReceivedCount(3); + assertThat(decHeaders.size(), is(3)); + verifyDecodedHeaders("foo", "bar", 3); + + resetState(); + addEncodeHeader("boo", "far", 3); + encode(out, encHeaders); + verifyRequiredInsertCount(6); + verifyKnownReceivedCount(0); // All acknowledged entries were removed. + + decode(out, decHeaders); + drainAllSuspendedEncoderInstructions(); + assertThat(decDynamicTable.insertCount(), is(6)); + verifyKnownReceivedCount(6); + assertThat(decHeaders.size(), is(3)); + verifyDecodedHeaders("boo", "far", 3); + + resetState(); + addEncodeHeader("zoo", "gar", 3); + encode(out, encHeaders); + verifyRequiredInsertCount(9); + verifyKnownReceivedCount(0); // All acknowledged entries were removed. + + decode(out, decHeaders); + drainAllSuspendedEncoderInstructions(); + verifyKnownReceivedCount(9); + assertThat(decDynamicTable.insertCount(), is(9)); + assertThat(decHeaders.size(), is(3)); + verifyDecodedHeaders("zoo", "gar", 3); + + // Now reuse the headers for encode to use dynamic table. + resetState(); + assertThat("Header not found in encoder dynamic table.", + encDynamicTable.getEntryIndex("zoo1", "gar"), greaterThanOrEqualTo(0)); + assertThat("Header not found in encoder dynamic table.", + encDynamicTable.getEntryIndex("zoo2", "gar"), greaterThanOrEqualTo(0)); + encHeaders.add("zoo1", "gar"); + encHeaders.add("zoo2", "gar"); + encode(out, encHeaders); + verifyRequiredInsertCount(9); // No new inserts + verifyKnownReceivedCount(9); // No new inserts + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(9)); + assertThat(decHeaders.size(), is(2)); + verifyDecodedHeader("zoo1", "gar"); + verifyDecodedHeader("zoo2", "gar"); + } + + @Test + public void duplicate() throws Exception { + setup(256, 0, 50); + // Do not ack any add so entries are not evicted from the table just marked for eviction hence leveraging + // duplicate path + stateSyncStrategyAckNextInsert = false; + + addEncodeHeader("foo", "bar", 5); + QpackHeaderField oldEntry = new QpackHeaderField("foo0", "bar"); + assertThat(encHeaders.get(oldEntry.name, oldEntry.value), is(notNullValue())); + + ByteBuf spareEncode = Unpooled.buffer(); + try { + encode(spareEncode, encHeaders); + } finally { + spareEncode.release(); + } + verifyRequiredInsertCount(5); + verifyKnownReceivedCount(0); + + final int idx = encDynamicTable.getEntryIndex(oldEntry.name, oldEntry.value); + assertThat(idx, greaterThanOrEqualTo(0)); + assertThat(encDynamicTable.requiresDuplication(idx, oldEntry.size()), is(true)); + + resetState(); + stateSyncStrategyAckNextInsert = true; + + encHeaders.add(oldEntry.name, oldEntry.value); + encode(out, encHeaders); // duplicate but not add to the header block + verifyRequiredInsertCount(6); + decode(out, decHeaders); + verifyKnownReceivedCount(6); + + assertThat(decDynamicTable.insertCount(), is(6)); + assertThat(decHeaders.size(), is(1)); + verifyDecodedHeader(oldEntry.name, oldEntry.value); + + // Now encode again to refer to the duplicated entry + resetState(); + + encHeaders.add(oldEntry.name, oldEntry.value); + encode(out, encHeaders); + verifyRequiredInsertCount(6); + decode(out, decHeaders); + verifyKnownReceivedCount(6); + + assertThat(decDynamicTable.insertCount(), is(6)); + assertThat(decHeaders.size(), is(1)); + verifyDecodedHeader(oldEntry.name, oldEntry.value); + } + + @Test + public void duplicateWithBlockedStreams() throws Exception { + setup(256, 100, 50); + // Do not ack any add so entries are not evicted from the table just marked for eviction hence leveraging + // duplicate path + stateSyncStrategyAckNextInsert = false; + + addEncodeHeader("foo", "bar", 5); + QpackHeaderField oldEntry = new QpackHeaderField("foo0", "bar"); + assertThat(encHeaders.get(oldEntry.name, oldEntry.value), is(notNullValue())); + + ByteBuf spareEncode = Unpooled.buffer(); + try { + encode(spareEncode, encHeaders); + } finally { + spareEncode.release(); + } + verifyRequiredInsertCount(5); + verifyKnownReceivedCount(0); + + final int idx = encDynamicTable.getEntryIndex(oldEntry.name, oldEntry.value); + assertThat(idx, greaterThanOrEqualTo(0)); + assertThat(encDynamicTable.requiresDuplication(idx, oldEntry.size()), is(true)); + + resetState(); + stateSyncStrategyAckNextInsert = true; + + encHeaders.add(oldEntry.name, oldEntry.value); + encode(out, encHeaders); + verifyRequiredInsertCount(6); + + drainNextSuspendedEncoderInstruction(); + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(0)); + verifyKnownReceivedCount(0); + + drainAllSuspendedEncoderInstructions(); + assertThat(decDynamicTable.insertCount(), is(6)); + verifyKnownReceivedCount(6); + assertThat(decHeaders.size(), is(1)); + verifyDecodedHeader(oldEntry.name, oldEntry.value); + } + + private void testDynamicTableIndexed(CharSequence name, CharSequence value) throws Exception { + encHeaders.add(name, value); + encode(out, encHeaders); + verifyRequiredInsertCount(headersAdded); + verifyKnownReceivedCount(headersAdded); + + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(headersAdded)); + assertThat(decHeaders.size(), is(1)); + verifyDecodedHeader(name, value); + + // Encode again to refer to dynamic table + out.clear(); + decHeaders.clear(); + + encode(out, encHeaders); + verifyRequiredInsertCount(headersAdded); + verifyKnownReceivedCount(headersAdded); + + decode(out, decHeaders); + assertThat(decDynamicTable.insertCount(), is(headersAdded)); + assertThat(decHeaders.size(), is(1)); + verifyDecodedHeader(name, value); + } + + private void testDynamicTableIndexedWithBlockedStreams(CharSequence name, CharSequence value) throws Exception { + encHeaders.add(name, value); + encode(out, encHeaders); + verifyRequiredInsertCount(headersAdded); + + verifyKnownReceivedCount(headersAdded - 1); + assertThat(decDynamicTable.insertCount(), is(headersAdded - 1)); + + drainNextSuspendedEncoderInstruction(); + + decode(out, decHeaders); + drainAllSuspendedEncoderInstructions(); + assertThat(decDynamicTable.insertCount(), is(headersAdded)); + verifyKnownReceivedCount(headersAdded); + + assertThat(decHeaders.size(), is(1)); + verifyDecodedHeader(name, value); + } + + @Test + public void staticTableOnly() throws Exception { + setup(0, 0); + + encHeaders.add(":authority", "netty.quic"); // name only + encHeaders.add(":path", "/"); // name & value + encHeaders.add(":method", "GET"); // name & value with few options per name + encHeaders.add(":status", "417"); // name & multiple values but value is missing + encHeaders.add("x-qpack-draft", "19"); + + encode(out, encHeaders); + decode(out, decHeaders); + + assertThat(decHeaders.size(), is(5)); + verifyDecodedHeader(":authority", "netty.quic"); + verifyDecodedHeader(":path", "/"); + verifyDecodedHeader(":method", "GET"); + verifyDecodedHeader(":status", "417"); + verifyDecodedHeader("x-qpack-draft", "19"); + } + + @Test + public void decoderThrowsOnInvalidInput() throws Exception { + setup(0, 0); + + encHeaders.add(":authority", "netty.quic"); // name only + encode(out, encHeaders); + // Add empty byte to the end of the buffer. This should trigger an exception in the decoder. + out.writeByte(0); + + assertThrows(QpackException.class, () -> decode(out, decHeaders)); + } + + private void resetState() { + out.clear(); + encHeaders.clear(); + decHeaders.clear(); + } + + private void encode(ByteBuf buf, Http3Headers headers) { + encoder.encodeHeaders(attributes, buf, DEFAULT, 1, headers); + assertThat("Parent channel closed.", parent.isActive(), is(true)); + } + + private void decode(ByteBuf buf, Http3Headers headers) throws QpackException { + decoder.decode(attributes, 1, buf, buf.readableBytes(), + new Http3HeadersSink(headers, 1024, false, false), () -> { + try { + decoder.decode(attributes, 1, buf, buf.readableBytes(), + new Http3HeadersSink(headers, 1024, false, false), + () -> { + throw new IllegalStateException("Decode resumption suspended."); + }); + } catch (QpackException e) { + throw new AssertionError("Decode failed.", e); + } + }); + assertThat("Parent channel closed.", parent.isActive(), is(true)); + } + + private void verifyDecodedHeader(CharSequence name, CharSequence value) { + assertThat(decHeaders.get(name), is(new AsciiString(value))); + } + + private void drainAllSuspendedEncoderInstructions() throws Exception { + Callable next; + for (next = suspendedEncoderInstructions.poll(); next != null; next = suspendedEncoderInstructions.poll()) { + next.call(); + } + } + + private void drainNextSuspendedEncoderInstruction() throws Exception { + Callable next = suspendedEncoderInstructions.poll(); + assertThat(next, is(notNullValue())); // dynamic table size instruction + next.call(); + } + + private void setup(long dynamicTableSize, int maxBlockedStreams) throws Exception { + setup(dynamicTableSize, maxBlockedStreams, 10); + } + + private void setup(long maxTableCapacity, int maxBlockedStreams, int expectedTableFreePercentage) throws Exception { + attributes = new QpackAttributes(parent, false); + Http3.setQpackAttributes(parent, attributes); + maxEntries = Math.toIntExact(QpackUtil.maxEntries(maxTableCapacity)); + DefaultHttp3SettingsFrame localSettings = new DefaultHttp3SettingsFrame(); + localSettings.put(HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY, maxTableCapacity); + localSettings.put(HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS, (long) maxBlockedStreams); + if (maxBlockedStreams > 0) { + // section acknowledgment will implicitly ack insert count. + stateSyncStrategyAckNextInsert = false; + } + when(syncStrategy.entryAdded(anyInt())).thenAnswer(__ -> stateSyncStrategyAckNextInsert); + encDynamicTable = new QpackEncoderDynamicTable(16, expectedTableFreePercentage); + decDynamicTable = new QpackDecoderDynamicTable(); + decoder = new QpackDecoder(maxTableCapacity, maxBlockedStreams, decDynamicTable, syncStrategy); + encoder = new QpackEncoder(encDynamicTable); + if (maxBlockedStreams > 0) { + suspendedEncoderInstructions = new LinkedBlockingQueue<>(); + } + EmbeddedQuicStreamChannel encoderStream = (EmbeddedQuicStreamChannel) parent.createStream(UNIDIRECTIONAL, + new ForwardWriteToReadOnOtherHandler(new QpackEncoderHandler(maxTableCapacity, decoder), + suspendedEncoderInstructions)).get(); + EmbeddedQuicStreamChannel decoderStream = (EmbeddedQuicStreamChannel) parent.createStream(UNIDIRECTIONAL, + new ForwardWriteToReadOnOtherHandler(new QpackDecoderHandler(encoder))).get(); + attributes.encoderStream(encoderStream); + attributes.decoderStream(decoderStream); + encoder.configureDynamicTable(attributes, maxTableCapacity, maxBlockedStreams); + } + + private void addEncodeHeader(String namePrefix, String value, int times) { + for (int i = 0; i < times; i++) { + encHeaders.add(namePrefix + i, value); + } + } + + private void verifyDecodedHeaders(String namePrefix, String value, int times) { + for (int i = 0; i < times; i++) { + verifyDecodedHeader(namePrefix + i, value); + } + } + + private void verifyRequiredInsertCount(int insertCount) { + assertThat("Unexpected dynamic table insert count.", + encDynamicTable.encodedRequiredInsertCount(encDynamicTable.insertCount()), + is(insertCount == 0 ? 0 : insertCount % (2 * maxEntries) + 1)); + } + + private void verifyKnownReceivedCount(int receivedCount) { + assertThat("Unexpected dynamic table known received count.", encDynamicTable.encodedKnownReceivedCount(), + is(receivedCount == 0 ? 0 : receivedCount % (2 * maxEntries) + 1)); + } + + private static final class ForwardWriteToReadOnOtherHandler extends ChannelOutboundHandlerAdapter { + + private final ChannelInboundHandler other; + private final BlockingQueue> suspendQueue; + + ForwardWriteToReadOnOtherHandler(ChannelInboundHandler other) { + this(other, null); + } + + ForwardWriteToReadOnOtherHandler(ChannelInboundHandler other, BlockingQueue> suspendQueue) { + this.other = other; + this.suspendQueue = suspendQueue; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ByteBuf) { + if (suspendQueue != null) { + suspendQueue.offer(() -> { + other.channelRead(ctx, msg); + return null; + }); + } else { + other.channelRead(ctx, msg); + } + } else { + super.write(ctx, msg, promise); + } + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackEncoderDynamicTableTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackEncoderDynamicTableTest.java new file mode 100644 index 0000000..2cea80d --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackEncoderDynamicTableTest.java @@ -0,0 +1,261 @@ +/* + * 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.handler.codec.http3; + +import org.junit.jupiter.api.Test; + +import static io.netty.handler.codec.http3.QpackUtil.MAX_HEADER_TABLE_SIZE; +import static java.lang.Math.toIntExact; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QpackEncoderDynamicTableTest { + private static final QpackHeaderField emptyHeader = new QpackHeaderField("", ""); + private static final QpackHeaderField fooBarHeader = new QpackHeaderField("foo", "bar"); + private static final QpackHeaderField fooBar2Header = new QpackHeaderField("foo", "bar2"); + private static final QpackHeaderField fooBar3Header = new QpackHeaderField("foo", "bar3"); + + private int insertCount; + private long maxCapacity; + + @Test + public void zeroCapacityIsAllowed() throws Exception { + QpackEncoderDynamicTable table = newDynamicTable(0); + + assertThat("Header addition passed.", addHeader(table, emptyHeader), + lessThan(0)); + } + + @Test + public void maxCapacityIsAllowed() throws Exception { + final QpackEncoderDynamicTable table = newDynamicTable(MAX_HEADER_TABLE_SIZE); + addAndValidateHeader(table, emptyHeader); + } + + @Test + public void negativeCapacityIsDisallowed() { + assertThrows(QpackException.class, () -> newDynamicTable(-1)); + } + + @Test + public void capacityTooLarge() { + assertThrows(QpackException.class, () -> newDynamicTable(Long.MAX_VALUE)); + } + + @Test + public void delayAck() throws Exception { + QpackEncoderDynamicTable table = newDynamicTable(16, 50, 128); + + addAndValidateHeader(table, emptyHeader); + addAndValidateHeader(table, fooBarHeader); + final int idx2 = addAndValidateHeader(table, fooBar2Header); + + assertThat("Header addition passed.", addHeader(table, fooBarHeader), lessThan(0)); + + table.incrementKnownReceivedCount(3); + assertThat("Unexpected entry index.", getEntryIndex(table, emptyHeader), lessThan(0)); + assertThat("Unexpected entry index.", getEntryIndex(table, fooBarHeader), lessThan(0)); + assertThat("Unexpected entry index.", getEntryIndex(table, fooBar2Header), is(idx2)); + + final int idx1 = addAndValidateHeader(table, emptyHeader); + assertThat("Unexpected entry index.", getEntryIndex(table, emptyHeader), is(idx1)); + assertThat("Unexpected entry index.", getEntryIndex(table, fooBar2Header), lessThan(0)); + } + + @Test + public void addAndGet() throws Exception { + QpackEncoderDynamicTable table = newDynamicTable(128); + + final int idx1 = addValidateAndAckHeader(table, emptyHeader); + assertEquals(0, idx1); + assertThat("Unexpected entry index.", getEntryIndex(table, emptyHeader), is(idx1)); + + final int idx2 = addValidateAndAckHeader(table, fooBarHeader); + assertEquals(1, idx2); + assertThat("Unexpected entry index.", getEntryIndex(table, fooBarHeader), is(idx2)); + + assertThat("Unexpected entry index.", getEntryIndex(table, emptyHeader), is(idx1)); + } + + @Test + public void nameOnlyMatch() throws Exception { + final QpackEncoderDynamicTable table = newDynamicTable(128); + addValidateAndAckHeader(table, fooBarHeader); + final int lastIdx = addValidateAndAckHeader(table, fooBar2Header); + + final int idx = table.getEntryIndex("foo", "baz"); + assertThat("Unexpected index.", idx, lessThan(0)); + assertThat("Unexpected index.", idx, is(-lastIdx - 1)); + } + + @Test + public void addDuplicateEntries() throws Exception { + QpackEncoderDynamicTable table = newDynamicTable(128); + + final int idx1 = addValidateAndAckHeader(table, emptyHeader); + assertThat("Unexpected entry index.", getEntryIndex(table, emptyHeader), is(idx1)); + + final int idx2 = addValidateAndAckHeader(table, fooBarHeader); + assertThat("Unexpected entry index.", getEntryIndex(table, fooBarHeader), is(idx2)); + + final int idx3 = addValidateAndAckHeader(table, emptyHeader); + // Return the most recent entry + assertThat("Unexpected entry index.", getEntryIndex(table, emptyHeader), is(idx3)); + } + + @Test + public void hashCollisionThenRemove() throws Exception { + // expected max size: 0.9*128 = 115 + QpackEncoderDynamicTable table = newDynamicTable(16, 10, 128); + addValidateAndAckHeader(table, fooBarHeader); // size = 38 + addValidateAndAckHeader(table, fooBar2Header); // size = 77 + + addValidateAndAckHeader(table, fooBar3Header); // size = 116, exceeds max threshold, should evict eldest + + assertThat("Entry found.", getEntryIndex(table, fooBarHeader), lessThan(0)); + assertThat("Entry not found.", getEntryIndex(table, fooBar2Header), greaterThanOrEqualTo(0)); + assertThat("Entry not found.", getEntryIndex(table, fooBar3Header), greaterThanOrEqualTo(0)); + } + + @Test + public void requiredInsertCountWrapsAround() throws Exception { + // maxIndex = 2 * maxEntries = 2 * 64/32 = 4 + QpackEncoderDynamicTable table = newDynamicTable(64); + + addValidateAndAckHeader(table, emptyHeader); + addValidateAndAckHeader(table, emptyHeader); + addValidateAndAckHeader(table, emptyHeader); + addValidateAndAckHeader(table, emptyHeader); + addValidateAndAckHeader(table, emptyHeader); + } + + @Test + public void indexWrapsAroundForSingleEntryCapacity() throws Exception { + // maxIndex = 2 * maxEntries = 2 * 39/32 = 2 + QpackEncoderDynamicTable table = newDynamicTable(fooBar2Header.size()); + addValidateAndAckHeader(table, fooBar2Header); + verifyTableEmpty(table); + addValidateAndAckHeader(table, fooBar2Header); + } + + @Test + public void sectionAck() throws Exception { + QpackEncoderDynamicTable table = newDynamicTable(128); + + final int idx = addAndValidateHeader(table, fooBarHeader); + table.addReferenceToEntry(fooBarHeader.name, fooBarHeader.value, idx); + table.acknowledgeInsertCountOnAck(idx); + + assertThat("Unexpected known received count.", table.encodedKnownReceivedCount(), is(2)); + } + + @Test + public void sectionAckOutOfOrder() throws Exception { + QpackEncoderDynamicTable table = newDynamicTable(128); + + final int idx1 = addAndValidateHeader(table, fooBarHeader); + table.addReferenceToEntry(fooBarHeader.name, fooBarHeader.value, idx1); + + final int idx2 = addAndValidateHeader(table, fooBarHeader); + table.addReferenceToEntry(fooBarHeader.name, fooBarHeader.value, idx2); + + table.acknowledgeInsertCountOnAck(idx2); + assertThat("Unexpected known received count.", table.encodedKnownReceivedCount(), is(3)); + + table.acknowledgeInsertCountOnAck(idx1); + assertThat("Unexpected known received count.", table.encodedKnownReceivedCount(), is(3)); // already acked + } + + @Test + public void multipleReferences() throws Exception { + // maxIndex = 2 * maxEntries = 2 * 39/32 = 2 + QpackEncoderDynamicTable table = newDynamicTable(fooBar3Header.size()); + + final int idx1 = addAndValidateHeader(table, fooBar3Header); + table.addReferenceToEntry(fooBar3Header.name, fooBar3Header.value, idx1); + table.addReferenceToEntry(fooBar3Header.name, fooBar3Header.value, idx1); + + table.acknowledgeInsertCountOnAck(idx1); + + // first entry still active + assertThat("Header added", addHeader(table, fooBar2Header), lessThan(0)); + + table.acknowledgeInsertCountOnAck(idx1); + verifyTableEmpty(table); + addAndValidateHeader(table, fooBarHeader); + } + + private void verifyTableEmpty(QpackEncoderDynamicTable table) { + assertThat(table.insertCount(), is(0)); + insertCount = 0; + } + + private int getEntryIndex(QpackEncoderDynamicTable table, QpackHeaderField emptyHeader) { + return table.getEntryIndex(emptyHeader.name, emptyHeader.value); + } + + private int addHeader(QpackEncoderDynamicTable table, QpackHeaderField header) { + final int idx = table.add(header.name, header.value, header.size()); + if (idx >= 0) { + insertCount++; + } + return idx; + } + + private int addAndValidateHeader(QpackEncoderDynamicTable table, QpackHeaderField header) { + final int addedIdx = addHeader(table, header); + assertThat("Header addition failed.", addedIdx, greaterThanOrEqualTo(0)); + verifyInsertCount(table); + return addedIdx; + } + + private int addValidateAndAckHeader(QpackEncoderDynamicTable table, QpackHeaderField header) throws Exception { + final int addedIdx = addAndValidateHeader(table, header); + table.addReferenceToEntry(header.name, header.value, addedIdx); + table.acknowledgeInsertCountOnAck(addedIdx); + return addedIdx; + } + + private QpackEncoderDynamicTable newDynamicTable(int arraySizeHint, int expectedFreeCapacityPercentage, + long maxCapacity) throws Exception { + return setMaxTableCapacity(maxCapacity, + new QpackEncoderDynamicTable(arraySizeHint, expectedFreeCapacityPercentage)); + } + + private QpackEncoderDynamicTable newDynamicTable(long maxCapacity) throws Exception { + return setMaxTableCapacity(maxCapacity, new QpackEncoderDynamicTable()); + } + + private QpackEncoderDynamicTable setMaxTableCapacity(long maxCapacity, QpackEncoderDynamicTable table) + throws Exception { + table.maxTableCapacity(maxCapacity); + this.maxCapacity = maxCapacity; + return table; + } + + private void verifyInsertCount(QpackEncoderDynamicTable table) { + assertThat("Unexpected required insert count.", + table.encodedRequiredInsertCount(table.insertCount()), is(expectedInsertCount())); + } + + private int expectedInsertCount() { + return insertCount == 0 ? 0 : toIntExact((insertCount % (2 * Math.floorDiv(maxCapacity, 32))) + 1); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackStaticTableTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackStaticTableTest.java new file mode 100644 index 0000000..9a0525f --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackStaticTableTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + + +public class QpackStaticTableTest { + @Test + public void testFieldNotFound() { + assertEquals(QpackStaticTable.NOT_FOUND, QpackStaticTable.findFieldIndex("x-netty-quic", "incubating")); + } + + @Test + public void testFieldNameAndValueMatch() { + // first in range + assertEquals(15, QpackStaticTable.findFieldIndex(":method", "CONNECT")); + // last in range + assertEquals(21, QpackStaticTable.findFieldIndex(":method", "PUT")); + // non-consequent range + assertEquals(24, QpackStaticTable.findFieldIndex(":status", "103")); + assertEquals(69, QpackStaticTable.findFieldIndex(":status", "421")); + } + + @Test + public void testFieldNameRefForEmptyField() { + int nameIndex1 = QpackStaticTable.findFieldIndex("cookie", "netty.io"); + int nameIndex2 = QpackStaticTable.findFieldIndex("cookie", "quic.io"); + + // should give the same name ref for any values + assertNotEquals(QpackStaticTable.NOT_FOUND, nameIndex1); + assertNotEquals(QpackStaticTable.NOT_FOUND, nameIndex2); + assertEquals(nameIndex1, nameIndex2); + + // index should be masked + assertEquals(nameIndex1 & QpackStaticTable.MASK_NAME_REF, QpackStaticTable.MASK_NAME_REF); + assertEquals(5, nameIndex1 ^ QpackStaticTable.MASK_NAME_REF); + } + + @Test + public void testFieldNameRefForSingleMatch() { + // note the value differs from static table ("1" rather than "0") + int nameIndex = QpackStaticTable.findFieldIndex("age", "1"); + assertEquals(2, nameIndex ^ QpackStaticTable.MASK_NAME_REF); + } + + @Test + public void testFieldNameRefForMultipleMatches() { + int nameIndex = QpackStaticTable.findFieldIndex(":method", "ALLTHETHINGS"); + assertEquals(15, nameIndex ^ QpackStaticTable.MASK_NAME_REF); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackStreamHandlerTest.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackStreamHandlerTest.java new file mode 100644 index 0000000..794bc4c --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/QpackStreamHandlerTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020 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.handler.codec.http3; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.quic.QuicStreamType; +import org.junit.jupiter.api.Test; + +import static io.netty.handler.codec.http3.Http3TestUtils.verifyClose; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class QpackStreamHandlerTest { + + @Test + public void testStreamClosedWhileParentStillActive() throws Exception { + EmbeddedQuicChannel parent = new EmbeddedQuicChannel(true); + + EmbeddedQuicStreamChannel channel = + (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, + new QpackDecoderHandler(new QpackEncoder())).get(); + assertFalse(channel.finish()); + Http3TestUtils.verifyClose(1, Http3ErrorCode.H3_CLOSED_CRITICAL_STREAM, parent); + } + + @Test + public void testStreamClosedWhileParentIsInactive() throws Exception { + EmbeddedQuicChannel parent = new EmbeddedQuicChannel(true); + parent.close().get(); + + EmbeddedQuicStreamChannel channel = + (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, + new QpackDecoderHandler(new QpackEncoder())).get(); + assertFalse(channel.finish()); + } + + @Test + public void testStreamDropsInboundData() throws Exception { + EmbeddedQuicChannel parent = new EmbeddedQuicChannel(true); + parent.close().get(); + + EmbeddedQuicStreamChannel channel = + (EmbeddedQuicStreamChannel) parent.createStream(QuicStreamType.UNIDIRECTIONAL, + new QpackDecoderHandler(new QpackEncoder())).get(); + ByteBuf buffer = Unpooled.buffer(); + assertFalse(channel.writeInbound(buffer)); + assertEquals(0, buffer.refCnt()); + assertFalse(channel.finish()); + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/example/Http3ClientExample.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/example/Http3ClientExample.java new file mode 100644 index 0000000..3647613 --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/example/Http3ClientExample.java @@ -0,0 +1,110 @@ +/* + * Copyright 2020 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.handler.codec.http3.example; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.codec.http3.DefaultHttp3HeadersFrame; +import io.netty.handler.codec.http3.Http3; +import io.netty.handler.codec.http3.Http3ClientConnectionHandler; +import io.netty.handler.codec.http3.Http3DataFrame; +import io.netty.handler.codec.http3.Http3HeadersFrame; +import io.netty.handler.codec.http3.Http3RequestStreamInboundHandler; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +public final class Http3ClientExample { + private Http3ClientExample() { } + + public static void main(String... args) throws Exception { + NioEventLoopGroup group = new NioEventLoopGroup(1); + + try { + QuicSslContext context = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(Http3.supportedApplicationProtocols()).build(); + ChannelHandler codec = Http3.newQuicClientCodecBuilder() + .sslContext(context) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .build(); + + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(0).sync().channel(); + + QuicChannel quicChannel = QuicChannel.newBootstrap(channel) + .handler(new Http3ClientConnectionHandler()) + .remoteAddress(new InetSocketAddress(NetUtil.LOCALHOST4, Http3ServerExample.PORT)) + .connect() + .get(); + + QuicStreamChannel streamChannel = Http3.newRequestStream(quicChannel, + new Http3RequestStreamInboundHandler() { + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3HeadersFrame frame) { + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3DataFrame frame) { + System.err.print(frame.content().toString(CharsetUtil.US_ASCII)); + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) { + ctx.close(); + } + }).sync().getNow(); + + // Write the Header frame and send the FIN to mark the end of the request. + // After this its not possible anymore to write any more data. + Http3HeadersFrame frame = new DefaultHttp3HeadersFrame(); + frame.headers().method("GET").path("/") + .authority(NetUtil.LOCALHOST4.getHostAddress() + ":" + Http3ServerExample.PORT) + .scheme("https"); + streamChannel.writeAndFlush(frame) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT).sync(); + + // Wait for the stream channel and quic channel to be closed (this will happen after we received the FIN). + // After this is done we will close the underlying datagram channel. + streamChannel.closeFuture().sync(); + + // After we received the response lets also close the underlying QUIC channel and datagram channel. + quicChannel.close().sync(); + channel.close().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/example/Http3ServerExample.java b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/example/Http3ServerExample.java new file mode 100644 index 0000000..3b1c09e --- /dev/null +++ b/netty-handler-codec-http3/src/test/java/io/netty/handler/codec/http3/example/Http3ServerExample.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 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.handler.codec.http3.example; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.handler.codec.http3.DefaultHttp3DataFrame; +import io.netty.handler.codec.http3.DefaultHttp3HeadersFrame; +import io.netty.handler.codec.http3.Http3; +import io.netty.handler.codec.http3.Http3DataFrame; +import io.netty.handler.codec.http3.Http3HeadersFrame; +import io.netty.handler.codec.http3.Http3RequestStreamInboundHandler; +import io.netty.handler.codec.http3.Http3ServerConnectionHandler; +import io.netty.handler.codec.quic.InsecureQuicTokenHandler; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +public final class Http3ServerExample { + private static final byte[] CONTENT = "Hello World!\r\n".getBytes(CharsetUtil.US_ASCII); + static final int PORT = 9999; + + private Http3ServerExample() { } + + public static void main(String... args) throws Exception { + int port; + // Allow to pass in the port so we can also use it to run h3spec against + if (args.length == 1) { + port = Integer.parseInt(args[0]); + } else { + port = PORT; + } + NioEventLoopGroup group = new NioEventLoopGroup(1); + SelfSignedCertificate cert = new SelfSignedCertificate(); + QuicSslContext sslContext = QuicSslContextBuilder.forServer(cert.key(), null, cert.cert()) + .applicationProtocols(Http3.supportedApplicationProtocols()).build(); + ChannelHandler codec = Http3.newQuicServerCodecBuilder() + .sslContext(sslContext) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(QuicChannel ch) { + // Called for each connection + ch.pipeline().addLast(new Http3ServerConnectionHandler( + new ChannelInitializer() { + // Called for each request-stream, + @Override + protected void initChannel(QuicStreamChannel ch) { + ch.pipeline().addLast(new Http3RequestStreamInboundHandler() { + + @Override + protected void channelRead( + ChannelHandlerContext ctx, Http3HeadersFrame frame) { + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelRead( + ChannelHandlerContext ctx, Http3DataFrame frame) { + ReferenceCountUtil.release(frame); + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) { + Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame(); + headersFrame.headers().status("404"); + headersFrame.headers().add("server", "netty"); + headersFrame.headers().addInt("content-length", CONTENT.length); + ctx.write(headersFrame); + ctx.writeAndFlush(new DefaultHttp3DataFrame( + Unpooled.wrappedBuffer(CONTENT))) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + }); + } + })); + } + }).build(); + try { + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(new InetSocketAddress(port)).sync().channel(); + channel.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/netty-handler-codec-http3/src/test/resources/logging.properties b/netty-handler-codec-http3/src/test/resources/logging.properties new file mode 100644 index 0000000..3cd7309 --- /dev/null +++ b/netty-handler-codec-http3/src/test/resources/logging.properties @@ -0,0 +1,7 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +jdk.event.security.level=INFO +org.junit.jupiter.engine.execution.ConditionEvaluator.level=OFF diff --git a/netty-handler-codec-quic/build.gradle b/netty-handler-codec-quic/build.gradle new file mode 100644 index 0000000..8e88e18 --- /dev/null +++ b/netty-handler-codec-quic/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':netty-handler-ssl') + implementation project(':netty-channel-epoll') + implementation project(':netty-channel-unix') +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSL.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSL.java new file mode 100644 index 0000000..46f0bc9 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSL.java @@ -0,0 +1,129 @@ +/* + * 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.handler.codec.quic; + +import io.netty.handler.ssl.util.LazyX509Certificate; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +final class BoringSSL { + private BoringSSL() { } + + static final int SSL_VERIFY_NONE = BoringSSLNativeStaticallyReferencedJniMethods.ssl_verify_none(); + static final int SSL_VERIFY_FAIL_IF_NO_PEER_CERT = BoringSSLNativeStaticallyReferencedJniMethods + .ssl_verify_fail_if_no_peer_cert(); + static final int SSL_VERIFY_PEER = BoringSSLNativeStaticallyReferencedJniMethods.ssl_verify_peer(); + static final int X509_V_OK = BoringSSLNativeStaticallyReferencedJniMethods.x509_v_ok(); + static final int X509_V_ERR_CERT_HAS_EXPIRED = + BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_cert_has_expired(); + static final int X509_V_ERR_CERT_NOT_YET_VALID = + BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_cert_not_yet_valid(); + static final int X509_V_ERR_CERT_REVOKED = BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_cert_revoked(); + static final int X509_V_ERR_UNSPECIFIED = BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_unspecified(); + + static long SSLContext_new(boolean server, String[] applicationProtocols, + BoringSSLHandshakeCompleteCallback handshakeCompleteCallback, + BoringSSLCertificateCallback certificateCallback, + BoringSSLCertificateVerifyCallback verifyCallback, + BoringSSLTlsextServernameCallback servernameCallback, + BoringSSLKeylogCallback keylogCallback, + BoringSSLSessionCallback sessionCallback, + BoringSSLPrivateKeyMethod privateKeyMethod, + BoringSSLSessionTicketCallback sessionTicketCallback, + int verifyMode, + byte[][] subjectNames) { + return SSLContext_new0(server, toWireFormat(applicationProtocols), + handshakeCompleteCallback, certificateCallback, verifyCallback, servernameCallback, + keylogCallback, sessionCallback, privateKeyMethod, sessionTicketCallback, verifyMode, subjectNames); + } + + private static byte[] toWireFormat(String[] applicationProtocols) { + if (applicationProtocols == null) { + return null; + } + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + for (String p : applicationProtocols) { + byte[] bytes = p.getBytes(StandardCharsets.US_ASCII); + out.write(bytes.length); + out.write(bytes); + } + return out.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static native long SSLContext_new0(boolean server, + byte[] applicationProtocols, Object handshakeCompleteCallback, + Object certificateCallback, Object verifyCallback, + Object servernameCallback, Object keylogCallback, + Object sessionCallback, + Object privateKeyMethod, + Object sessionTicketCallback, + 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); + static native long SSLContext_setSessionCacheTimeout(long context, long size); + + static native void SSLContext_setSessionTicketKeys(long context, boolean enableCallback); + + static native void SSLContext_free(long context); + static long SSL_new(long context, boolean server, String hostname) { + return SSL_new0(context, server, tlsExtHostName(hostname)); + } + static native long SSL_new0(long context, boolean server, String hostname); + static native void SSL_free(long ssl); + + static native Runnable SSL_getTask(long ssl); + + static native void SSL_cleanup(long ssl); + + static native long EVP_PKEY_parse(byte[] bytes, String pass); + static native void EVP_PKEY_free(long key); + + static native long CRYPTO_BUFFER_stack_new(long ssl, byte[][] bytes); + static native void CRYPTO_BUFFER_stack_free(long chain); + + static native String ERR_last_error(); + + private static String tlsExtHostName(String hostname) { + if (hostname != null && hostname.endsWith(".")) { + // Strip trailing dot if included. + // See https://github.com/netty/netty-tcnative/issues/400 + hostname = hostname.substring(0, hostname.length() - 1); + } + return hostname; + } + + static X509Certificate[] certificates(byte[][] chain) { + X509Certificate[] peerCerts = new X509Certificate[chain.length]; + for (int i = 0; i < peerCerts.length; i++) { + peerCerts[i] = new LazyX509Certificate(chain[i]); + } + return peerCerts; + } + + static byte[][] subjectNames(X509Certificate[] certificates) { + byte[][] subjectNames = new byte[certificates.length][]; + for (int i = 0; i < certificates.length; i++) { + subjectNames[i] = certificates[i].getSubjectX500Principal().getEncoded(); + } + return subjectNames; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLAsyncPrivateKeyMethod.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLAsyncPrivateKeyMethod.java new file mode 100644 index 0000000..c4a87b6 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLAsyncPrivateKeyMethod.java @@ -0,0 +1,57 @@ +/* + * 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.handler.codec.quic; + +import io.netty.util.concurrent.Future; + +import javax.net.ssl.SSLEngine; + +public interface BoringSSLAsyncPrivateKeyMethod { + int SSL_SIGN_RSA_PKCS1_SHA1 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA1; + int SSL_SIGN_RSA_PKCS1_SHA256 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256; + int SSL_SIGN_RSA_PKCS1_SHA384 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384; + int SSL_SIGN_RSA_PKCS1_SHA512 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512; + int SSL_SIGN_ECDSA_SHA1 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SHA1; + int SSL_SIGN_ECDSA_SECP256R1_SHA256 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256; + int SSL_SIGN_ECDSA_SECP384R1_SHA384 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384; + int SSL_SIGN_ECDSA_SECP521R1_SHA512 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512; + int SSL_SIGN_RSA_PSS_RSAE_SHA256 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256; + int SSL_SIGN_RSA_PSS_RSAE_SHA384 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384; + int SSL_SIGN_RSA_PSS_RSAE_SHA512 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512; + int SSL_SIGN_ED25519 = BoringSSLPrivateKeyMethod.SSL_SIGN_ED25519; + int SSL_SIGN_RSA_PKCS1_MD5_SHA1 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_MD5_SHA1; + + /** + * Signs the input with the given key and notifies the returned {@link Future} with the signed bytes. + * + * @param engine the {@link SSLEngine} + * @param signatureAlgorithm the algorithm to use for signing + * @param input the digest itself + * @return the {@link Future} that will be notified with the signed data + * (must not be {@code null}) when the operation completes. + */ + Future sign(SSLEngine engine, int signatureAlgorithm, byte[] input); + + /** + * Decrypts the input with the given key and notifies the returned {@link Future} with the decrypted bytes. + * + * @param engine the {@link SSLEngine} + * @param input the input which should be decrypted + * @return the {@link Future} that will be notified with the decrypted data + * (must not be {@code null}) when the operation completes. + */ + Future decrypt(SSLEngine engine, byte[] input); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallback.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallback.java new file mode 100644 index 0000000..d251676 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallback.java @@ -0,0 +1,276 @@ +/* + * 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.handler.codec.quic; + + +import io.netty.util.CharsetUtil; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.security.auth.x500.X500Principal; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +final class BoringSSLCertificateCallback { + private static final byte[] BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n".getBytes(CharsetUtil.US_ASCII); + private static final byte[] END_PRIVATE_KEY = "\n-----END PRIVATE KEY-----\n".getBytes(CharsetUtil.US_ASCII); + + /** + * The types contained in the {@code keyTypeBytes} array. + */ + // Extracted from https://github.com/openssl/openssl/blob/master/include/openssl/tls1.h + private static final byte TLS_CT_RSA_SIGN = 1; + private static final byte TLS_CT_DSS_SIGN = 2; + private static final byte TLS_CT_RSA_FIXED_DH = 3; + private static final byte TLS_CT_DSS_FIXED_DH = 4; + private static final byte TLS_CT_ECDSA_SIGN = 64; + private static final byte TLS_CT_RSA_FIXED_ECDH = 65; + private static final byte TLS_CT_ECDSA_FIXED_ECDH = 66; + + // Code in this class is inspired by code of conscrypts: + // - https://android.googlesource.com/platform/external/ + // conscrypt/+/master/src/main/java/org/conscrypt/OpenSSLEngineImpl.java + // - https://android.googlesource.com/platform/external/ + // conscrypt/+/master/src/main/java/org/conscrypt/SSLParametersImpl.java + // + static final String KEY_TYPE_RSA = "RSA"; + static final String KEY_TYPE_DH_RSA = "DH_RSA"; + static final String KEY_TYPE_EC = "EC"; + static final String KEY_TYPE_EC_EC = "EC_EC"; + static final String KEY_TYPE_EC_RSA = "EC_RSA"; + + // key type mappings for types. + private static final Map KEY_TYPES = new HashMap(); + static { + KEY_TYPES.put("RSA", KEY_TYPE_RSA); + KEY_TYPES.put("DHE_RSA", KEY_TYPE_RSA); + KEY_TYPES.put("ECDHE_RSA", KEY_TYPE_RSA); + KEY_TYPES.put("ECDHE_ECDSA", KEY_TYPE_EC); + KEY_TYPES.put("ECDH_RSA", KEY_TYPE_EC_RSA); + KEY_TYPES.put("ECDH_ECDSA", KEY_TYPE_EC_EC); + KEY_TYPES.put("DH_RSA", KEY_TYPE_DH_RSA); + } + + private static final Set SUPPORTED_KEY_TYPES = Collections.unmodifiableSet(new LinkedHashSet<>( + Arrays.asList(KEY_TYPE_RSA, + KEY_TYPE_DH_RSA, + KEY_TYPE_EC, + KEY_TYPE_EC_RSA, + KEY_TYPE_EC_EC))); + + // Directly returning this is safe as we never modify it within our JNI code. + private static final long[] NO_KEY_MATERIAL_CLIENT_SIDE = new long[] { 0, 0 }; + + private final QuicheQuicSslEngineMap engineMap; + private final X509ExtendedKeyManager keyManager; + private final String password; + + BoringSSLCertificateCallback(QuicheQuicSslEngineMap engineMap, X509ExtendedKeyManager keyManager, String password) { + this.engineMap = engineMap; + this.keyManager = keyManager; + this.password = password; + } + + @SuppressWarnings("unused") + long[] handle(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals, String[] authMethods) { + QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + return null; + } + + try { + if (keyManager == null) { + if (engine.getUseClientMode()) { + return NO_KEY_MATERIAL_CLIENT_SIDE; + } + return null; + } + if (engine.getUseClientMode()) { + final Set keyTypesSet = supportedClientKeyTypes(keyTypeBytes); + final String[] keyTypes = keyTypesSet.toArray(new String[0]); + final X500Principal[] issuers; + if (asn1DerEncodedPrincipals == null) { + issuers = null; + } else { + issuers = new X500Principal[asn1DerEncodedPrincipals.length]; + for (int i = 0; i < asn1DerEncodedPrincipals.length; i++) { + issuers[i] = new X500Principal(asn1DerEncodedPrincipals[i]); + } + } + return removeMappingIfNeeded(ssl, selectKeyMaterialClientSide(ssl, engine, keyTypes, issuers)); + } else { + // For now we just ignore the asn1DerEncodedPrincipals as this is kind of inline with what the + // OpenJDK SSLEngineImpl does. + return removeMappingIfNeeded(ssl, selectKeyMaterialServerSide(ssl, engine, authMethods)); + } + } catch (SSLException e) { + engineMap.remove(ssl); + return null; + } catch (Throwable cause) { + engineMap.remove(ssl); + throw cause; + } + } + + private long[] removeMappingIfNeeded(long ssl, long[] result) { + if (result == null) { + engineMap.remove(ssl); + } + return result; + } + + private long[] selectKeyMaterialServerSide(long ssl, QuicheQuicSslEngine engine, String[] authMethods) + throws SSLException { + if (authMethods.length == 0) { + throw new SSLHandshakeException("Unable to find key material"); + } + + // authMethods may contain duplicates or may result in the same type + // but call chooseServerAlias(...) may be expensive. So let's ensure + // we filter out duplicates. + Set typeSet = new HashSet(KEY_TYPES.size()); + for (String authMethod : authMethods) { + String type = KEY_TYPES.get(authMethod); + if (type != null && typeSet.add(type)) { + String alias = chooseServerAlias(engine, type); + if (alias != null) { + return selectMaterial(ssl, engine, alias) ; + } + } + } + throw new SSLHandshakeException("Unable to find key material for auth method(s): " + + Arrays.toString(authMethods)); + } + + private long[] selectKeyMaterialClientSide(long ssl, QuicheQuicSslEngine engine, String[] keyTypes, + X500Principal[] issuer) { + String alias = chooseClientAlias(engine, keyTypes, issuer); + // Only try to set the keymaterial if we have a match. This is also consistent with what OpenJDK does: + // https://hg.openjdk.java.net/jdk/jdk11/file/76072a077ee1/ + // src/java.base/share/classes/sun/security/ssl/CertificateRequest.java#l362 + if (alias != null) { + return selectMaterial(ssl, engine, alias) ; + } + return NO_KEY_MATERIAL_CLIENT_SIDE; + } + + private long[] selectMaterial(long ssl, QuicheQuicSslEngine engine, String alias) { + X509Certificate[] certificates = keyManager.getCertificateChain(alias); + if (certificates == null || certificates.length == 0) { + return null; + } + byte[][] certs = new byte[certificates.length][]; + + for (int i = 0; i < certificates.length; i++) { + try { + certs[i] = certificates[i].getEncoded(); + } catch (CertificateEncodingException e) { + return null; + } + } + + final long key; + PrivateKey privateKey = keyManager.getPrivateKey(alias); + if (privateKey == BoringSSLKeylessPrivateKey.INSTANCE) { + key = 0; + } else { + byte[] pemKey = toPemEncoded(privateKey); + if (pemKey == null) { + return null; + } + key = BoringSSL.EVP_PKEY_parse(pemKey, password); + } + long chain = BoringSSL.CRYPTO_BUFFER_stack_new(ssl, certs); + engine.setLocalCertificateChain(certificates); + + // Return and signal that the key and chain should be released as well. + return new long[] { key, chain }; + } + + private static byte[] toPemEncoded(PrivateKey key) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + out.write(BEGIN_PRIVATE_KEY); + out.write(Base64.getEncoder().encode(key.getEncoded())); + out.write(END_PRIVATE_KEY); + return out.toByteArray(); + } catch (IOException e) { + return null; + } + } + private String chooseClientAlias(QuicheQuicSslEngine engine, + String[] keyTypes, X500Principal[] issuer) { + return keyManager.chooseEngineClientAlias(keyTypes, issuer, engine); + } + + private String chooseServerAlias(QuicheQuicSslEngine engine, String type) { + return keyManager.chooseEngineServerAlias(type, null, engine); + } + + /** + * Gets the supported key types for client certificates. + * + * @param clientCertificateTypes {@code ClientCertificateType} values provided by the server. + * See https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml. + * @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and + * {@code X509ExtendedKeyManager.chooseEngineClientAlias}. + */ + private static Set supportedClientKeyTypes(byte[] clientCertificateTypes) { + if (clientCertificateTypes == null) { + // Try all of the supported key types. + return SUPPORTED_KEY_TYPES; + } + Set result = new HashSet<>(clientCertificateTypes.length); + for (byte keyTypeCode : clientCertificateTypes) { + String keyType = clientKeyType(keyTypeCode); + if (keyType == null) { + // Unsupported client key type -- ignore + continue; + } + result.add(keyType); + } + return result; + } + + private static String clientKeyType(byte clientCertificateType) { + // See also https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml + switch (clientCertificateType) { + case TLS_CT_RSA_SIGN: + return KEY_TYPE_RSA; // RFC rsa_sign + case TLS_CT_RSA_FIXED_DH: + return KEY_TYPE_DH_RSA; // RFC rsa_fixed_dh + case TLS_CT_ECDSA_SIGN: + return KEY_TYPE_EC; // RFC ecdsa_sign + case TLS_CT_RSA_FIXED_ECDH: + return KEY_TYPE_EC_RSA; // RFC rsa_fixed_ecdh + case TLS_CT_ECDSA_FIXED_ECDH: + return KEY_TYPE_EC_EC; // RFC ecdsa_fixed_ecdh + default: + return null; + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallbackTask.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallbackTask.java new file mode 100644 index 0000000..7c57f6a --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallbackTask.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 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: + * + * http://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.handler.codec.quic; + +/** + * Execute {@link BoringSSLCertificateCallback#handle(long, byte[], byte[][], String[])}. + */ +final class BoringSSLCertificateCallbackTask extends BoringSSLTask { + private final byte[] keyTypeBytes; + private final byte[][] asn1DerEncodedPrincipals; + private final String[] authMethods; + private final BoringSSLCertificateCallback callback; + + // Accessed via JNI. + private long key; + private long chain; + + BoringSSLCertificateCallbackTask(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals, String[] authMethods, + BoringSSLCertificateCallback callback) { + // It is important that this constructor never throws. Be sure to not change this! + super(ssl); + // It's ok to not clone the arrays as we create these in JNI and not-reuse. + this.keyTypeBytes = keyTypeBytes; + this.asn1DerEncodedPrincipals = asn1DerEncodedPrincipals; + this.authMethods = authMethods; + this.callback = callback; + } + + // See https://www.openssl.org/docs/man1.0.2/man3/SSL_set_cert_cb.html. + @Override + protected void runTask(long ssl, TaskCallback taskCallback) { + try { + long[] result = callback.handle(ssl, keyTypeBytes, asn1DerEncodedPrincipals, authMethods); + if (result == null) { + taskCallback.onResult(ssl, 0); + } else { + this.key = result[0]; + this.chain = result[1]; + taskCallback.onResult(ssl, 1); + } + } catch (Exception e) { + // Just catch the exception and return 0 to fail the handshake. + // The problem is that rethrowing here is really "useless" as we will process it as part of an openssl + // c callback which needs to return 0 for an error to abort the handshake. + taskCallback.onResult(ssl, 0); + } + } + + @Override + protected void destroy() { + if (key != 0) { + BoringSSL.EVP_PKEY_free(key); + key = 0; + } + if (chain != 0) { + BoringSSL.CRYPTO_BUFFER_stack_free(chain); + chain = 0; + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallback.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallback.java new file mode 100644 index 0000000..509ba35 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallback.java @@ -0,0 +1,124 @@ +/* + * 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.handler.codec.quic; + +import io.netty.handler.ssl.OpenSslCertificateException; + +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateRevokedException; +import java.security.cert.X509Certificate; + +final class BoringSSLCertificateVerifyCallback { + + private static final boolean TRY_USING_EXTENDED_TRUST_MANAGER; + static { + boolean tryUsingExtendedTrustManager; + try { + Class.forName(X509ExtendedTrustManager.class.getName()); + tryUsingExtendedTrustManager = true; + } catch (Throwable cause) { + tryUsingExtendedTrustManager = false; + } + TRY_USING_EXTENDED_TRUST_MANAGER = tryUsingExtendedTrustManager; + } + + private final QuicheQuicSslEngineMap engineMap; + private final X509TrustManager manager; + + BoringSSLCertificateVerifyCallback(QuicheQuicSslEngineMap engineMap, X509TrustManager manager) { + this.engineMap = engineMap; + this.manager = manager; + } + + @SuppressWarnings("unused") + int verify(long ssl, byte[][] x509, String authAlgorithm) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + return BoringSSL.X509_V_ERR_UNSPECIFIED; + } + + if (manager == null) { + engineMap.remove(ssl); + return BoringSSL.X509_V_ERR_UNSPECIFIED; + } + + X509Certificate[] peerCerts = BoringSSL.certificates(x509); + try { + if (engine.getUseClientMode()) { + if (TRY_USING_EXTENDED_TRUST_MANAGER && manager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) manager).checkServerTrusted(peerCerts, authAlgorithm, engine); + } else { + manager.checkServerTrusted(peerCerts, authAlgorithm); + } + } else { + if (TRY_USING_EXTENDED_TRUST_MANAGER && manager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) manager).checkClientTrusted(peerCerts, authAlgorithm, engine); + } else { + manager.checkClientTrusted(peerCerts, authAlgorithm); + } + } + return BoringSSL.X509_V_OK; + } catch (Throwable cause) { + engineMap.remove(ssl); + // Try to extract the correct error code that should be used. + if (cause instanceof OpenSslCertificateException) { + // This will never return a negative error code as its validated when constructing the + // OpenSslCertificateException. + return ((OpenSslCertificateException) cause).errorCode(); + } + if (cause instanceof CertificateExpiredException) { + return BoringSSL.X509_V_ERR_CERT_HAS_EXPIRED; + } + if (cause instanceof CertificateNotYetValidException) { + return BoringSSL.X509_V_ERR_CERT_NOT_YET_VALID; + } + return translateToError(cause); + } + } + + private static int translateToError(Throwable cause) { + if (cause instanceof CertificateRevokedException) { + return BoringSSL.X509_V_ERR_CERT_REVOKED; + } + + // The X509TrustManagerImpl uses a Validator which wraps a CertPathValidatorException into + // an CertificateException. So we need to handle the wrapped CertPathValidatorException to be + // able to send the correct alert. + Throwable wrapped = cause.getCause(); + while (wrapped != null) { + if (wrapped instanceof CertPathValidatorException) { + CertPathValidatorException ex = (CertPathValidatorException) wrapped; + CertPathValidatorException.Reason reason = ex.getReason(); + if (reason == CertPathValidatorException.BasicReason.EXPIRED) { + return BoringSSL.X509_V_ERR_CERT_HAS_EXPIRED; + } + if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) { + return BoringSSL.X509_V_ERR_CERT_NOT_YET_VALID; + } + if (reason == CertPathValidatorException.BasicReason.REVOKED) { + return BoringSSL.X509_V_ERR_CERT_REVOKED; + } + } + wrapped = wrapped.getCause(); + } + return BoringSSL.X509_V_ERR_UNSPECIFIED; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallbackTask.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallbackTask.java new file mode 100644 index 0000000..5a45dc0 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallbackTask.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 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: + * + * http://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.handler.codec.quic; + + +/** + * Execute {@link BoringSSLCertificateVerifyCallback#verify(long, byte[][], String)}. + */ +final class BoringSSLCertificateVerifyCallbackTask extends BoringSSLTask { + private final byte[][] x509; + private final String authAlgorithm; + private final BoringSSLCertificateVerifyCallback verifier; + + BoringSSLCertificateVerifyCallbackTask(long ssl, byte[][] x509, String authAlgorithm, + BoringSSLCertificateVerifyCallback verifier) { + super(ssl); + this.x509 = x509; + this.authAlgorithm = authAlgorithm; + this.verifier = verifier; + } + + @Override + protected void runTask(long ssl, TaskCallback callback) { + int result = verifier.verify(ssl, x509, authAlgorithm); + callback.onResult(ssl, result); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLHandshakeCompleteCallback.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLHandshakeCompleteCallback.java new file mode 100644 index 0000000..b44ef71 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLHandshakeCompleteCallback.java @@ -0,0 +1,36 @@ +/* + * 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.handler.codec.quic; + +final class BoringSSLHandshakeCompleteCallback { + + private final QuicheQuicSslEngineMap map; + + BoringSSLHandshakeCompleteCallback(QuicheQuicSslEngineMap map) { + this.map = map; + } + + @SuppressWarnings("unused") + void handshakeComplete(long ssl, byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout, byte[] applicationProtocol, + boolean sessionReused) { + QuicheQuicSslEngine engine = map.get(ssl); + if (engine != null) { + engine.handshakeFinished(id, cipher, protocol, peerCertificate, peerCertificateChain, creationTime, + timeout, applicationProtocol, sessionReused); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessManagerFactory.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessManagerFactory.java new file mode 100644 index 0000000..65cb430 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessManagerFactory.java @@ -0,0 +1,245 @@ +/* + * Copyright 2022 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.handler.codec.quic; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManagerFactorySpi; +import javax.net.ssl.ManagerFactoryParameters; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.util.Objects.requireNonNull; + +/** + * {@link KeyManagerFactory} that can be used to support custom key signing via {@link BoringSSLAsyncPrivateKeyMethod}. + */ +public final class BoringSSLKeylessManagerFactory extends KeyManagerFactory { + + final BoringSSLAsyncPrivateKeyMethod privateKeyMethod; + + private BoringSSLKeylessManagerFactory(KeyManagerFactory keyManagerFactory, + BoringSSLAsyncPrivateKeyMethod privateKeyMethod) { + super(new KeylessManagerFactorySpi(keyManagerFactory), + keyManagerFactory.getProvider(), keyManagerFactory.getAlgorithm()); + this.privateKeyMethod = requireNonNull(privateKeyMethod, "privateKeyMethod"); + } + + /** + * Creates a new factory instance. + * + * @param privateKeyMethod the {@link BoringSSLAsyncPrivateKeyMethod} that is used for key signing. + * @param chain the {@link File} that contains the {@link X509Certificate} chain. + * @return a new factory instance. + * @throws CertificateException on error. + * @throws IOException on error. + * @throws KeyStoreException on error. + * @throws NoSuchAlgorithmException on error. + * @throws UnrecoverableKeyException on error. + */ + public static BoringSSLKeylessManagerFactory newKeyless(BoringSSLAsyncPrivateKeyMethod privateKeyMethod, File chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(privateKeyMethod, Files.newInputStream(chain.toPath())); + } + + /** + * Creates a new factory instance. + * + * @param privateKeyMethod the {@link BoringSSLAsyncPrivateKeyMethod} that is used for key signing. + * @param chain the {@link InputStream} that contains the {@link X509Certificate} chain. + * @return a new factory instance. + * @throws CertificateException on error. + * @throws IOException on error. + * @throws KeyStoreException on error. + * @throws NoSuchAlgorithmException on error. + * @throws UnrecoverableKeyException on error. + */ + public static BoringSSLKeylessManagerFactory newKeyless(BoringSSLAsyncPrivateKeyMethod privateKeyMethod, + InputStream chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(privateKeyMethod, QuicSslContext.toX509Certificates0(chain)); + } + + /** + * Creates a new factory instance. + * + * @param privateKeyMethod the {@link BoringSSLAsyncPrivateKeyMethod} that is used for key signing. + * @param certificateChain the {@link X509Certificate} chain. + * @return a new factory instance. + * @throws CertificateException on error. + * @throws IOException on error. + * @throws KeyStoreException on error. + * @throws NoSuchAlgorithmException on error. + * @throws UnrecoverableKeyException on error. + */ + public static BoringSSLKeylessManagerFactory newKeyless(BoringSSLAsyncPrivateKeyMethod privateKeyMethod, + X509Certificate... certificateChain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + checkNotNull(certificateChain, "certificateChain"); + KeyStore store = new KeylessKeyStore(certificateChain.clone()); + store.load(null, null); + BoringSSLKeylessManagerFactory factory = new BoringSSLKeylessManagerFactory( + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()), privateKeyMethod); + factory.init(store, null); + return factory; + } + + private static final class KeylessManagerFactorySpi extends KeyManagerFactorySpi { + + private final KeyManagerFactory keyManagerFactory; + + KeylessManagerFactorySpi(KeyManagerFactory keyManagerFactory) { + this.keyManagerFactory = requireNonNull(keyManagerFactory, "keyManagerFactory"); + } + + @Override + protected void engineInit(KeyStore ks, char[] password) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + keyManagerFactory.init(ks, password); + } + + @Override + protected void engineInit(ManagerFactoryParameters spec) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + return keyManagerFactory.getKeyManagers(); + } + } + private static final class KeylessKeyStore extends KeyStore { + private static final String ALIAS = "key"; + private KeylessKeyStore(final X509Certificate[] certificateChain) { + super(new KeyStoreSpi() { + + private final Date creationDate = new Date(); + + @Override + public Key engineGetKey(String alias, char[] password) { + if (engineContainsAlias(alias)) { + return BoringSSLKeylessPrivateKey.INSTANCE; + } + return null; + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + return engineContainsAlias(alias)? certificateChain.clone() : null; + } + + @Override + public Certificate engineGetCertificate(String alias) { + return engineContainsAlias(alias)? certificateChain[0] : null; + } + + @Override + public Date engineGetCreationDate(String alias) { + return engineContainsAlias(alias)? creationDate : null; + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) + throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public Enumeration engineAliases() { + return Collections.enumeration(Collections.singleton(ALIAS)); + } + + @Override + public boolean engineContainsAlias(String alias) { + return ALIAS.equals(alias); + } + + @Override + public int engineSize() { + return 1; + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return engineContainsAlias(alias); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return engineContainsAlias(alias); + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + if (cert instanceof X509Certificate) { + for (X509Certificate x509Certificate : certificateChain) { + if (x509Certificate.equals(cert)) { + return ALIAS; + } + } + } + return null; + } + + @Override + public void engineStore(OutputStream stream, char[] password) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineLoad(InputStream stream, char[] password) { + if (stream != null && password != null) { + throw new UnsupportedOperationException(); + } + } + }, null, "keyless"); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessPrivateKey.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessPrivateKey.java new file mode 100644 index 0000000..2a19eac --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessPrivateKey.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 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.handler.codec.quic; + +import io.netty.util.internal.EmptyArrays; + +import java.security.PrivateKey; + +final class BoringSSLKeylessPrivateKey implements PrivateKey { + + static final BoringSSLKeylessPrivateKey INSTANCE = new BoringSSLKeylessPrivateKey(); + + private BoringSSLKeylessPrivateKey() { + } + + @Override + public String getAlgorithm() { + return "keyless"; + } + + @Override + public String getFormat() { + return "keyless"; + } + + @Override + public byte[] getEncoded() { + return EmptyArrays.EMPTY_BYTES; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylog.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylog.java new file mode 100644 index 0000000..7d8e356 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylog.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 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.handler.codec.quic; + +import javax.net.ssl.SSLEngine; + + +/** + * Allow to log keys, logging keys are following + * + * NSS Key Log Format. This is intended for debugging use with tools like Wireshark. + */ +public interface BoringSSLKeylog { + + /** + * Called when a key should be logged. + * + * @param engine the engine. + * @param key the key. + */ + void logKey(SSLEngine engine, String key); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylogCallback.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylogCallback.java new file mode 100644 index 0000000..6ffdce2 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylogCallback.java @@ -0,0 +1,38 @@ +/* + * 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.handler.codec.quic; + + +import javax.net.ssl.SSLEngine; + +final class BoringSSLKeylogCallback { + + private final QuicheQuicSslEngineMap engineMap; + private final BoringSSLKeylog keylog; + + BoringSSLKeylogCallback(QuicheQuicSslEngineMap engineMap, BoringSSLKeylog keylog) { + this.engineMap = engineMap; + this.keylog = keylog; + } + + @SuppressWarnings("unused") + void logKey(long ssl, String key) { + SSLEngine engine = engineMap.get(ssl); + if (engine != null) { + keylog.logKey(engine, key); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLLoggingKeylog.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLLoggingKeylog.java new file mode 100644 index 0000000..3e78dce --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLLoggingKeylog.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 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.handler.codec.quic; + +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.net.ssl.SSLEngine; + +final class BoringSSLLoggingKeylog implements BoringSSLKeylog { + static final BoringSSLLoggingKeylog INSTANCE = new BoringSSLLoggingKeylog(); + + private BoringSSLLoggingKeylog() { + } + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(BoringSSLLoggingKeylog.class); + + @Override + public void logKey(SSLEngine engine, String key) { + logger.debug(key); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLNativeStaticallyReferencedJniMethods.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLNativeStaticallyReferencedJniMethods.java new file mode 100644 index 0000000..771d0b8 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLNativeStaticallyReferencedJniMethods.java @@ -0,0 +1,43 @@ +/* + * 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.handler.codec.quic; + +final class BoringSSLNativeStaticallyReferencedJniMethods { + static native int ssl_verify_none(); + static native int ssl_verify_peer(); + static native int ssl_verify_fail_if_no_peer_cert(); + + static native int x509_v_ok(); + static native int x509_v_err_cert_has_expired(); + static native int x509_v_err_cert_not_yet_valid(); + static native int x509_v_err_cert_revoked(); + static native int x509_v_err_unspecified(); + static native int ssl_sign_rsa_pkcs_sha1(); + static native int ssl_sign_rsa_pkcs_sha256(); + static native int ssl_sign_rsa_pkcs_sha384(); + static native int ssl_sign_rsa_pkcs_sha512(); + static native int ssl_sign_ecdsa_pkcs_sha1(); + static native int ssl_sign_ecdsa_secp256r1_sha256(); + static native int ssl_sign_ecdsa_secp384r1_sha384(); + static native int ssl_sign_ecdsa_secp521r1_sha512(); + static native int ssl_sign_rsa_pss_rsae_sha256(); + static native int ssl_sign_rsa_pss_rsae_sha384(); + static native int ssl_sign_rsa_pss_rsae_sha512(); + static native int ssl_sign_ed25519(); + static native int ssl_sign_rsa_pkcs1_md5_sha1(); + + private BoringSSLNativeStaticallyReferencedJniMethods() { } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod.java new file mode 100644 index 0000000..691d6fa --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 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: + * + * http://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.handler.codec.quic; + +import java.util.function.BiConsumer; + +/** + * Allows to customize private key signing / decrypt (when using RSA). + */ +interface BoringSSLPrivateKeyMethod { + int SSL_SIGN_RSA_PKCS1_SHA1 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha1(); + int SSL_SIGN_RSA_PKCS1_SHA256 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha256(); + int SSL_SIGN_RSA_PKCS1_SHA384 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha384(); + int SSL_SIGN_RSA_PKCS1_SHA512 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha512(); + int SSL_SIGN_ECDSA_SHA1 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_pkcs_sha1(); + int SSL_SIGN_ECDSA_SECP256R1_SHA256 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_secp256r1_sha256(); + int SSL_SIGN_ECDSA_SECP384R1_SHA384 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_secp384r1_sha384(); + int SSL_SIGN_ECDSA_SECP521R1_SHA512 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_secp521r1_sha512(); + int SSL_SIGN_RSA_PSS_RSAE_SHA256 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pss_rsae_sha256(); + int SSL_SIGN_RSA_PSS_RSAE_SHA384 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pss_rsae_sha384(); + int SSL_SIGN_RSA_PSS_RSAE_SHA512 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pss_rsae_sha512(); + int SSL_SIGN_ED25519 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ed25519(); + int SSL_SIGN_RSA_PKCS1_MD5_SHA1 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs1_md5_sha1(); + + /** + * Sign the input with given EC key and returns the signed bytes. + * + * @param ssl the SSL instance + * @param signatureAlgorithm the algorithm to use for signing + * @param input the input itself + * @return the sign + * @throws Exception thrown if an error accours while signing. + */ + void sign(long ssl, int signatureAlgorithm, byte[] input, BiConsumer callback); + + /** + * Decrypts the input with the given RSA key and returns the decrypted bytes. + * + * @param ssl the SSL instance + * @param input the input which should be decrypted + * @return the decrypted data + * @throws Exception thrown if an error accours while decrypting. + */ + void decrypt(long ssl, byte[] input, BiConsumer callback); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodDecryptTask.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodDecryptTask.java new file mode 100644 index 0000000..de50246 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodDecryptTask.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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: + * + * http://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.handler.codec.quic; + +import java.util.function.BiConsumer; + +final class BoringSSLPrivateKeyMethodDecryptTask extends BoringSSLPrivateKeyMethodTask { + private final byte[] input; + + BoringSSLPrivateKeyMethodDecryptTask(long ssl, byte[] input, BoringSSLPrivateKeyMethod method) { + super(ssl, method); + // It's OK to not clone the arrays as we create these in JNI and not reuse. + this.input = input; + } + + @Override + protected void runMethod(long ssl, BoringSSLPrivateKeyMethod method, BiConsumer consumer) { + method.decrypt(ssl, input, consumer); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodSignTask.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodSignTask.java new file mode 100644 index 0000000..d98952f --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodSignTask.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 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: + * + * http://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.handler.codec.quic; + +import java.util.function.BiConsumer; + +final class BoringSSLPrivateKeyMethodSignTask extends BoringSSLPrivateKeyMethodTask { + private final int signatureAlgorithm; + private final byte[] digest; + + BoringSSLPrivateKeyMethodSignTask(long ssl, int signatureAlgorithm, byte[] digest, BoringSSLPrivateKeyMethod method) { + super(ssl, method); + this.signatureAlgorithm = signatureAlgorithm; + // It's OK to not clone the arrays as we create these in JNI and not reuse. + this.digest = digest; + } + + @Override + protected void runMethod(long ssl, BoringSSLPrivateKeyMethod method, BiConsumer callback) { + method.sign(ssl, signatureAlgorithm, digest, callback); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodTask.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodTask.java new file mode 100644 index 0000000..31ba8ab --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodTask.java @@ -0,0 +1,32 @@ +package io.netty.handler.codec.quic; + +import java.util.function.BiConsumer; + +abstract class BoringSSLPrivateKeyMethodTask extends BoringSSLTask { + + private final BoringSSLPrivateKeyMethod method; + + // Will be accessed via JNI. + private byte[] resultBytes; + + BoringSSLPrivateKeyMethodTask(long ssl, BoringSSLPrivateKeyMethod method) { + super(ssl); + this.method = method; + } + + + @Override + protected final void runTask(long ssl, TaskCallback callback) { + runMethod(ssl, method, (result, error) -> { + if (result == null || error != null) { + callback.onResult(ssl, -1); + } else { + resultBytes = result; + callback.onResult(ssl, 1); + } + }); + } + + protected abstract void runMethod(long ssl, BoringSSLPrivateKeyMethod method, + BiConsumer callback); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionCallback.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionCallback.java new file mode 100644 index 0000000..dab5f72 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/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.handler.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/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionTicketCallback.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionTicketCallback.java new file mode 100644 index 0000000..1395f94 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionTicketCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import io.netty.util.internal.PlatformDependent; + +final class BoringSSLSessionTicketCallback { + + // As we dont assume to have a lot of keys configured we will just use an array for now as a data store. + private volatile byte[][] sessionKeys; + + // Accessed via JNI. + byte[] findSessionTicket(byte[] keyname) { + byte[][] keys = this.sessionKeys; + if (keys == null || keys.length == 0) { + return null; + } + if (keyname == null) { + return keys[0]; + } + + for (int i = 0; i < keys.length; i++) { + byte[] key = keys[i]; + if (PlatformDependent.equals(keyname, 0, key, 1, keyname.length)) { + return key; + } + } + return null; + } + + void setSessionTicketKeys(SslSessionTicketKey[] keys) { + if (keys != null && keys.length != 0) { + byte[][] sessionKeys = new byte[keys.length][]; + for(int i = 0; i < keys.length; ++i) { + SslSessionTicketKey key = keys[i]; + byte[] binaryKey = new byte[49]; + // We mark the first key as preferred by using 1 as byte marker + binaryKey[0] = i == 0 ? (byte) 1 : (byte) 0; + int dstCurPos = 1; + System.arraycopy(key.name, 0, binaryKey, dstCurPos, 16); + dstCurPos += 16; + System.arraycopy(key.hmacKey, 0, binaryKey, dstCurPos, 16); + dstCurPos += 16; + System.arraycopy(key.aesKey, 0, binaryKey, dstCurPos, 16); + sessionKeys[i] = binaryKey; + } + this.sessionKeys = sessionKeys; + } else { + sessionKeys = null; + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTask.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTask.java new file mode 100644 index 0000000..c739d88 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTask.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 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: + * + * http://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.handler.codec.quic; + +/** + * A SSL related task that will be returned by {@link BoringSSL#SSL_getTask(long)}. + */ +abstract class BoringSSLTask implements Runnable { + private final long ssl; + protected boolean didRun; + + // These fields are accessed via JNI. + private int returnValue; + private volatile boolean complete; + + protected BoringSSLTask(long ssl) { + // It is important that this constructor never throws. Be sure to not change this! + this.ssl = ssl; + } + + @Override + public final void run() { + if (!didRun) { + didRun = true; + runTask(ssl, (long ssl, int result) -> { + returnValue = result; + complete = true; + }); + } + } + + /** + * Called once the task should be destroyed. + */ + protected void destroy() { + // Noop + } + + /** + * Run the task and return the return value that should be passed back to OpenSSL. + */ + protected abstract void runTask(long ssl, TaskCallback callback); + + interface TaskCallback { + void onResult(long ssl, int result); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTlsextServernameCallback.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTlsextServernameCallback.java new file mode 100644 index 0000000..324ca6d --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTlsextServernameCallback.java @@ -0,0 +1,45 @@ +/* + * 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.handler.codec.quic; + +import io.netty.util.Mapping; + +final class BoringSSLTlsextServernameCallback { + + private final QuicheQuicSslEngineMap engineMap; + private final Mapping mapping; + + BoringSSLTlsextServernameCallback(QuicheQuicSslEngineMap engineMap, + Mapping mapping) { + this.engineMap = engineMap; + this.mapping = mapping; + } + + @SuppressWarnings("unused") + long selectCtx(long ssl, String serverName) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + return -1; + } + + QuicSslContext context = mapping.map(serverName); + if (context == null) { + return -1; + } + return engine.moveTo(serverName, (QuicheQuicSslContext) context); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/DefaultQuicStreamFrame.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/DefaultQuicStreamFrame.java new file mode 100644 index 0000000..ac78d13 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/DefaultQuicStreamFrame.java @@ -0,0 +1,110 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; + +public final class DefaultQuicStreamFrame extends DefaultByteBufHolder implements QuicStreamFrame { + + private final boolean fin; + + public DefaultQuicStreamFrame(ByteBuf data, boolean fin) { + super(data); + this.fin = fin; + } + + @Override + public boolean hasFin() { + return fin; + } + + @Override + public QuicStreamFrame copy() { + return new DefaultQuicStreamFrame(content().copy(), fin); + } + + @Override + public QuicStreamFrame duplicate() { + return new DefaultQuicStreamFrame(content().duplicate(), fin); + } + + @Override + public QuicStreamFrame retainedDuplicate() { + return new DefaultQuicStreamFrame(content().retainedDuplicate(), fin); + } + + @Override + public QuicStreamFrame replace(ByteBuf content) { + return new DefaultQuicStreamFrame(content, fin); + } + + @Override + public QuicStreamFrame retain() { + super.retain(); + return this; + } + + @Override + public QuicStreamFrame retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public QuicStreamFrame touch() { + super.touch(); + return this; + } + + @Override + public QuicStreamFrame touch(Object hint) { + super.touch(hint); + return this; + } + + @Override + public String toString() { + return "DefaultQuicStreamFrame{" + + "fin=" + fin + + ", content=" + contentToString() + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultQuicStreamFrame that = (DefaultQuicStreamFrame) o; + + if (fin != that.fin) { + return false; + } + + return super.equals(o); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (fin ? 1 : 0); + return result; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/DirectIoByteBufAllocator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/DirectIoByteBufAllocator.java new file mode 100644 index 0000000..72a7fe6 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/DirectIoByteBufAllocator.java @@ -0,0 +1,136 @@ +/* + * 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; + +final class DirectIoByteBufAllocator implements ByteBufAllocator { + + private final ByteBufAllocator wrapped; + + DirectIoByteBufAllocator(ByteBufAllocator wrapped) { + if (wrapped instanceof DirectIoByteBufAllocator) { + wrapped = ((DirectIoByteBufAllocator) wrapped).wrapped(); + } + this.wrapped = wrapped; + } + + ByteBufAllocator wrapped() { + return wrapped; + } + + @Override + public ByteBuf buffer() { + return wrapped.buffer(); + } + + @Override + public ByteBuf buffer(int initialCapacity) { + return wrapped.buffer(initialCapacity); + } + + @Override + public ByteBuf buffer(int initialCapacity, int maxCapacity) { + return wrapped.buffer(initialCapacity, maxCapacity); + } + + @Override + public ByteBuf ioBuffer() { + return directBuffer(); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity) { + return directBuffer(initialCapacity); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) { + return directBuffer(initialCapacity, maxCapacity); + } + + @Override + public ByteBuf heapBuffer() { + return wrapped.heapBuffer(); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity) { + return wrapped.heapBuffer(initialCapacity); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity, int maxCapacity) { + return wrapped.heapBuffer(initialCapacity, maxCapacity); + } + + @Override + public ByteBuf directBuffer() { + return wrapped.directBuffer(); + } + + @Override + public ByteBuf directBuffer(int initialCapacity) { + return wrapped.directBuffer(initialCapacity); + } + + @Override + public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { + return wrapped.directBuffer(initialCapacity, maxCapacity); + } + + @Override + public CompositeByteBuf compositeBuffer() { + return wrapped.compositeBuffer(); + } + + @Override + public CompositeByteBuf compositeBuffer(int maxNumComponents) { + return wrapped.compositeBuffer(maxNumComponents); + } + + @Override + public CompositeByteBuf compositeHeapBuffer() { + return wrapped.compositeHeapBuffer(); + } + + @Override + public CompositeByteBuf compositeHeapBuffer(int maxNumComponents) { + return wrapped.compositeHeapBuffer(maxNumComponents); + } + + @Override + public CompositeByteBuf compositeDirectBuffer() { + return wrapped.compositeDirectBuffer(); + } + + @Override + public CompositeByteBuf compositeDirectBuffer(int maxNumComponents) { + return wrapped.compositeDirectBuffer(maxNumComponents); + } + + @Override + public boolean isDirectBufferPooled() { + return wrapped.isDirectBufferPooled(); + } + + @Override + public int calculateNewCapacity(int minNewCapacity, int maxCapacity) { + return wrapped.calculateNewCapacity(minNewCapacity, maxCapacity); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/EpollQuicUtils.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/EpollQuicUtils.java new file mode 100644 index 0000000..2e8b94f --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/EpollQuicUtils.java @@ -0,0 +1,65 @@ +/* + * 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.epoll.SegmentedDatagramPacket; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.internal.ObjectUtil; + +import java.net.InetSocketAddress; + +/** + * Class that provides utility methods to setup {@code QUIC} when using the {@code EPOLL} transport. + */ +public final class EpollQuicUtils { + + private EpollQuicUtils() { } + + /** + * Return a new {@link SegmentedDatagramPacketAllocator} that can be used while using + * {@link io.netty.channel.epoll.EpollDatagramChannel}. + * + * @param maxNumSegments the maximum number of segments that we try to send in one packet. + * @return a allocator. + */ + public static SegmentedDatagramPacketAllocator newSegmentedAllocator(int maxNumSegments) { + ObjectUtil.checkInRange(maxNumSegments, 1, 64, "maxNumSegments"); + if (SegmentedDatagramPacket.isSupported()) { + return new EpollSegmentedDatagramPacketAllocator(maxNumSegments); + } + return SegmentedDatagramPacketAllocator.NONE; + } + + private static final class EpollSegmentedDatagramPacketAllocator implements SegmentedDatagramPacketAllocator { + + private final int maxNumSegments; + + EpollSegmentedDatagramPacketAllocator(int maxNumSegments) { + this.maxNumSegments = maxNumSegments; + } + + @Override + public int maxNumSegments() { + return maxNumSegments; + } + + @Override + public DatagramPacket newPacket(ByteBuf buffer, int segmentSize, InetSocketAddress remoteAddress) { + return new io.netty.channel.unix.SegmentedDatagramPacket(buffer, segmentSize, remoteAddress); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/FlushStrategy.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/FlushStrategy.java new file mode 100644 index 0000000..e4a441c --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/FlushStrategy.java @@ -0,0 +1,60 @@ +/* + * 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.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +/** + * Allows to configure a strategy for when flushes should be happening. + */ +public interface FlushStrategy { + + /** + * Default {@link FlushStrategy} implementation. + */ + FlushStrategy DEFAULT = afterNumBytes(20 * Quic.MAX_DATAGRAM_SIZE); + + /** + * Returns {@code true} if a flush should happen now, {@code false} otherwise. + * + * @param numPackets the number of packets that were written since the last flush. + * @param numBytes the number of bytes that were written since the last flush. + * @return {@code true} if a flush should be done now, {@code false} otherwise. + */ + boolean shouldFlushNow(int numPackets, int numBytes); + + /** + * Implementation that flushes after a number of bytes. + * + * @param bytes the number of bytes after which we should issue a flush. + * @return the {@link FlushStrategy}. + */ + static FlushStrategy afterNumBytes(int bytes) { + ObjectUtil.checkPositive(bytes, "bytes"); + return (numPackets, numBytes) -> numBytes > bytes; + } + + /** + * Implementation that flushes after a number of packets. + * + * @param packets the number of packets after which we should issue a flush. + * @return the {@link FlushStrategy}. + */ + static FlushStrategy afterNumPackets(int packets) { + ObjectUtil.checkPositive(packets, "packets"); + return (numPackets, numBytes) -> numPackets > packets; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Hmac.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Hmac.java new file mode 100644 index 0000000..cad5897 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Hmac.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import io.netty.util.concurrent.FastThreadLocal; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +final class Hmac { + + private static final FastThreadLocal MACS = new FastThreadLocal() { + @Override + protected Mac initialValue() { + return newMac(); + } + }; + + private static final String ALGORITM = "HmacSHA256"; + private static final byte[] randomKey = new byte[16]; + + static { + new SecureRandom().nextBytes(randomKey); + } + + private static Mac newMac() { + try { + SecretKeySpec keySpec = new SecretKeySpec(randomKey, ALGORITM); + Mac mac = Mac.getInstance(ALGORITM); + mac.init(keySpec); + return mac; + } catch (NoSuchAlgorithmException | InvalidKeyException exception) { + throw new IllegalStateException(exception); + } + } + + static ByteBuffer sign(ByteBuffer input, int outLength) { + Mac mac = MACS.get(); + mac.reset(); + mac.update(input); + byte[] signBytes = mac.doFinal(); + if (signBytes.length != outLength) { + signBytes = Arrays.copyOf(signBytes, outLength); + } + return ByteBuffer.wrap(signBytes); + } + + private Hmac() { } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicConnectionIdGenerator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicConnectionIdGenerator.java new file mode 100644 index 0000000..db7147b --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicConnectionIdGenerator.java @@ -0,0 +1,56 @@ +/* + * 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.handler.codec.quic; + +import java.nio.ByteBuffer; + +import io.netty.util.internal.ObjectUtil; + +/** + * A {@link QuicConnectionIdGenerator} which creates new connection id by signing the given input + * using hmac algorithms. + */ +final class HmacSignQuicConnectionIdGenerator implements QuicConnectionIdGenerator { + static final QuicConnectionIdGenerator INSTANCE = new HmacSignQuicConnectionIdGenerator(); + + private HmacSignQuicConnectionIdGenerator() { + } + + @Override + public ByteBuffer newId(int length) { + throw new UnsupportedOperationException( + "HmacSignQuicConnectionIdGenerator should always have an input to sign with"); + } + + @Override + public ByteBuffer newId(ByteBuffer buffer, int length) { + ObjectUtil.checkNotNull(buffer, "buffer"); + ObjectUtil.checkPositive(buffer.remaining(), "buffer"); + ObjectUtil.checkInRange(length, 0, maxConnectionIdLength(), "length"); + + return Hmac.sign(buffer, length); + } + + @Override + public int maxConnectionIdLength() { + return Quiche.QUICHE_MAX_CONN_ID_LEN; + } + + @Override + public boolean isIdempotent() { + return true; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicResetTokenGenerator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicResetTokenGenerator.java new file mode 100644 index 0000000..e52e6e5 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicResetTokenGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +import java.nio.ByteBuffer; + +/** + * A {@link QuicResetTokenGenerator} which creates new reset token by using the connection id by signing the given input + * using HMAC algorithms. + */ +final class HmacSignQuicResetTokenGenerator implements QuicResetTokenGenerator { + static final QuicResetTokenGenerator INSTANCE = new HmacSignQuicResetTokenGenerator(); + + private HmacSignQuicResetTokenGenerator() { + } + + + @Override + public ByteBuffer newResetToken(ByteBuffer cid) { + ObjectUtil.checkNotNull(cid, "cid"); + ObjectUtil.checkPositive(cid.remaining(), "cid"); + return Hmac.sign(cid, Quic.RESET_TOKEN_LEN); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/InsecureQuicTokenHandler.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/InsecureQuicTokenHandler.java new file mode 100644 index 0000000..278e08c --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/InsecureQuicTokenHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; + +import java.net.InetSocketAddress; + +/** + * Insecure {@link QuicTokenHandler} which only does basic token generation / validation without any + * crypto. + * + * This shouldn't be used in production. + */ +public final class InsecureQuicTokenHandler implements QuicTokenHandler { + + private static final String SERVER_NAME = "netty"; + private static final byte[] SERVER_NAME_BYTES = SERVER_NAME.getBytes(CharsetUtil.US_ASCII); + private static final ByteBuf SERVER_NAME_BUFFER = Unpooled.unreleasableBuffer( + Unpooled.wrappedBuffer(SERVER_NAME_BYTES)).asReadOnly(); + + // Just package-private for unit tests + static final int MAX_TOKEN_LEN = Quiche.QUICHE_MAX_CONN_ID_LEN + + NetUtil.LOCALHOST6.getAddress().length + SERVER_NAME_BYTES.length; + + private InsecureQuicTokenHandler() { + Quic.ensureAvailability(); + } + + public static final InsecureQuicTokenHandler INSTANCE = new InsecureQuicTokenHandler(); + + @Override + public boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address) { + byte[] addr = address.getAddress().getAddress(); + out.writeBytes(SERVER_NAME_BYTES) + .writeBytes(addr) + .writeBytes(dcid, dcid.readerIndex(), dcid.readableBytes()); + return true; + } + + @Override + public int validateToken(ByteBuf token, InetSocketAddress address) { + final byte[] addr = address.getAddress().getAddress(); + + int minLength = SERVER_NAME_BYTES.length + address.getAddress().getAddress().length; + if (token.readableBytes() <= SERVER_NAME_BYTES.length + addr.length) { + return -1; + } + + if (!SERVER_NAME_BUFFER.equals(token.slice(0, SERVER_NAME_BYTES.length))) { + return -1; + } + ByteBuf addressBuffer = Unpooled.wrappedBuffer(addr); + try { + if (!addressBuffer.equals(token.slice(SERVER_NAME_BYTES.length, addr.length))) { + return -1; + } + } finally { + addressBuffer.release(); + } + return minLength; + } + + @Override + public int maxTokenLength() { + return MAX_TOKEN_LEN; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/NoQuicTokenHandler.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/NoQuicTokenHandler.java new file mode 100644 index 0000000..695dd23 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/NoQuicTokenHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; + +import java.net.InetSocketAddress; + +/** + * {@link QuicTokenHandler} which will disable token generation / validation completely. + * This will reduce the round-trip for QUIC connection migration, but will also weaking the + * security during connection establishment. + */ +final class NoQuicTokenHandler implements QuicTokenHandler { + + public final static QuicTokenHandler INSTANCE = new NoQuicTokenHandler(); + + private NoQuicTokenHandler() { + } + + @Override + public boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address) { + return false; + } + + @Override + public int validateToken(ByteBuf token, InetSocketAddress address) { + return 0; + } + + @Override + public int maxTokenLength() { + return 0; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QLogConfiguration.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QLogConfiguration.java new file mode 100644 index 0000000..0c99db8 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QLogConfiguration.java @@ -0,0 +1,70 @@ +/* + * 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.handler.codec.quic; + +import java.util.Objects; + +/** + * Configuration used for setup + * qlog. + */ +public final class QLogConfiguration { + + private final String path; + private final String logTitle; + private final String logDescription; + + /** + * Create a new configuration. + * + * @param path the path to the log file to use. This file must not exist yet. If the path is a + * directory the filename will be generated + * @param logTitle the title to use when logging. + * @param logDescription the description to use when logging. + */ + public QLogConfiguration(String path, String logTitle, String logDescription) { + this.path = Objects.requireNonNull(path, "path"); + this.logTitle = Objects.requireNonNull(logTitle, "logTitle"); + this.logDescription = Objects.requireNonNull(logDescription, "logDescription"); + } + + /** + * Return the path to the log file. + * + * @return the path. + */ + public String path() { + return path; + } + + /** + * Return the title. + * + * @return the title. + */ + public String logTitle() { + return logTitle; + } + + /** + * Return the description. + * + * @return the description. + */ + public String logDescription() { + return logDescription; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Quic.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Quic.java new file mode 100644 index 0000000..0e51d0d --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Quic.java @@ -0,0 +1,167 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class Quic { + @SuppressWarnings("unchecked") + static final Map.Entry, Object>[] EMPTY_OPTION_ARRAY = new Map.Entry[0]; + @SuppressWarnings("unchecked") + static final Map.Entry, Object>[] EMPTY_ATTRIBUTE_ARRAY = new Map.Entry[0]; + + static final int MAX_DATAGRAM_SIZE = 1350; + + static final int RESET_TOKEN_LEN = 16; + + private static final Throwable UNAVAILABILITY_CAUSE; + + static { + Throwable cause = null; + + try { + String version = Quiche.quiche_version(); + assert version != null; + } catch (Throwable error) { + cause = error; + } + + UNAVAILABILITY_CAUSE = cause; + } + + /** + * Return if the given QUIC version is supported. + * + * @param version the version. + * @return {@code true} if supported, {@code false} otherwise. + */ + public static boolean isVersionSupported(int version) { + return isAvailable() && Quiche.quiche_version_is_supported(version); + } + + /** + * Returns {@code true} if and only if the QUIC implementation is usable on the running platform is available. + * + * @return {@code true} if this QUIC implementation can be used on the current platform, {@code false} otherwise. + */ + public static boolean isAvailable() { + return UNAVAILABILITY_CAUSE == null; + } + + /** + * Ensure that QUIC implementation is usable on the running platform is available. + * + * @throws UnsatisfiedLinkError if unavailable + */ + public static void ensureAvailability() { + if (UNAVAILABILITY_CAUSE != null) { + throw (Error) new UnsatisfiedLinkError( + "failed to load the required native library").initCause(UNAVAILABILITY_CAUSE); + } + } + + /** + * Returns the cause of unavailability. + * + * @return the cause if unavailable. {@code null} if available. + */ + public static Throwable unavailabilityCause() { + return UNAVAILABILITY_CAUSE; + } + + static Map.Entry, Object>[] toOptionsArray(Map, Object> opts) { + return new HashMap<>(opts).entrySet().toArray(EMPTY_OPTION_ARRAY); + } + + static Map.Entry, Object>[] toAttributesArray(Map, Object> attributes) { + return new LinkedHashMap<>(attributes).entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY); + } + + private static void setAttributes(Channel channel, Map.Entry, Object>[] attrs) { + for (Map.Entry, Object> e: attrs) { + @SuppressWarnings("unchecked") + AttributeKey key = (AttributeKey) e.getKey(); + channel.attr(key).set(e.getValue()); + } + } + + private static void setChannelOptions( + Channel channel, Map.Entry, Object>[] options, InternalLogger logger) { + for (Map.Entry, Object> e: options) { + setChannelOption(channel, e.getKey(), e.getValue(), logger); + } + } + + @SuppressWarnings("unchecked") + private static void setChannelOption( + Channel channel, ChannelOption option, Object value, InternalLogger logger) { + try { + if (!channel.config().setOption((ChannelOption) option, value)) { + logger.warn("Unknown channel option '{}' for channel '{}'", option, channel); + } + } catch (Throwable t) { + logger.warn( + "Failed to set channel option '{}' with value '{}' for channel '{}'", option, value, channel, t); + } + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + */ + static void updateOptions(Map, Object> options, ChannelOption option, T value) { + ObjectUtil.checkNotNull(option, "option"); + if (value == null) { + options.remove(option); + } else { + options.put(option, value); + } + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + */ + static void updateAttributes(Map, Object> attributes, AttributeKey key, T value) { + ObjectUtil.checkNotNull(key, "key"); + if (value == null) { + attributes.remove(key); + } else { + attributes.put(key, value); + } + } + + static void setupChannel(Channel ch, Map.Entry, Object>[] options, + Map.Entry, Object>[] attrs, ChannelHandler handler, + InternalLogger logger) { + Quic.setChannelOptions(ch, options, logger); + Quic.setAttributes(ch, attrs); + if (handler != null) { + ch.pipeline().addLast(handler); + } + } + + private Quic() { } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannel.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannel.java new file mode 100644 index 0000000..a8394a1 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannel.java @@ -0,0 +1,274 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; + +/** + * A QUIC {@link Channel}. + */ +public interface QuicChannel extends Channel { + + @Override + default ChannelFuture bind(SocketAddress localAddress) { + return pipeline().bind(localAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress) { + return pipeline().connect(remoteAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { + return pipeline().connect(remoteAddress, localAddress); + } + + @Override + default ChannelFuture disconnect() { + return pipeline().disconnect(); + } + + @Override + default ChannelFuture close() { + return pipeline().close(); + } + + @Override + default ChannelFuture deregister() { + return pipeline().deregister(); + } + + @Override + default ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { + return pipeline().bind(localAddress, promise); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { + return pipeline().connect(remoteAddress, promise); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + return pipeline().connect(remoteAddress, localAddress, promise); + } + + @Override + default ChannelFuture disconnect(ChannelPromise promise) { + return pipeline().disconnect(promise); + } + + @Override + default ChannelFuture close(ChannelPromise promise) { + return pipeline().close(promise); + } + + @Override + default ChannelFuture deregister(ChannelPromise promise) { + return pipeline().deregister(promise); + } + + @Override + default ChannelFuture write(Object msg) { + return pipeline().write(msg); + } + + @Override + default ChannelFuture write(Object msg, ChannelPromise promise) { + return pipeline().write(msg, promise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return pipeline().writeAndFlush(msg, promise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg) { + return pipeline().writeAndFlush(msg); + } + + @Override + default ChannelPromise newPromise() { + return pipeline().newPromise(); + } + + @Override + default ChannelProgressivePromise newProgressivePromise() { + return pipeline().newProgressivePromise(); + } + + @Override + default ChannelFuture newSucceededFuture() { + return pipeline().newSucceededFuture(); + } + + @Override + default ChannelFuture newFailedFuture(Throwable cause) { + return pipeline().newFailedFuture(cause); + } + + @Override + default ChannelPromise voidPromise() { + return pipeline().voidPromise(); + } + + @Override + QuicChannel read(); + + @Override + QuicChannel flush(); + + /** + * Returns the configuration of this channel. + */ + @Override + QuicChannelConfig config(); + + /** + * Returns the used {@link SSLEngine} or {@code null} if none is used (yet). + * + * @return the engine. + */ + SSLEngine sslEngine(); + + /** + * Returns the number of streams that can be created before stream creation will fail + * with {@link QuicError#STREAM_LIMIT} error. + * + * @param type the stream type. + * @return the number of streams left. + */ + long peerAllowedStreams(QuicStreamType type); + + /** + * Returns {@code true} if the connection was closed because of idle timeout. + * + * @return {@code true} if the connection was closed because of idle timeout, {@code false}. + */ + boolean isTimedOut(); + + /** + * Returns the {@link QuicTransportParameters} of the peer once received, or {@code null} if not known yet. + * + * @return peerTransportParams. + */ + QuicTransportParameters peerTransportParameters(); + + /** + * Creates a stream that is using this {@link QuicChannel} and notifies the {@link Future} once done. + * The {@link ChannelHandler} (if not {@code null}) is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} automatically. + * + * @param type the {@link QuicStreamType} of the {@link QuicStreamChannel}. + * @param handler the {@link ChannelHandler} that will be added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline} during the stream creation. + * @return the {@link Future} that will be notified once the operation completes. + */ + default Future createStream(QuicStreamType type, ChannelHandler handler) { + return createStream(type, handler, eventLoop().newPromise()); + } + + /** + * Creates a stream that is using this {@link QuicChannel} and notifies the {@link Promise} once done. + * The {@link ChannelHandler} (if not {@code null}) is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} automatically. + * + * @param type the {@link QuicStreamType} of the {@link QuicStreamChannel}. + * @param handler the {@link ChannelHandler} that will be added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline} during the stream creation. + * @param promise the {@link ChannelPromise} that will be notified once the operation completes. + * @return the {@link Future} that will be notified once the operation completes. + */ + Future createStream(QuicStreamType type, ChannelHandler handler, + Promise promise); + + /** + * Returns a new {@link QuicStreamChannelBootstrap} which makes it easy to bootstrap new {@link QuicStreamChannel}s + * with custom options and attributes. For simpler use-cases you may want to consider using + * {@link #createStream(QuicStreamType, ChannelHandler)} or + * {@link #createStream(QuicStreamType, ChannelHandler, Promise)} directly. + * + * @return {@link QuicStreamChannelBootstrap} that can be used to bootstrap a {@link QuicStreamChannel}. + */ + default QuicStreamChannelBootstrap newStreamBootstrap() { + return new QuicStreamChannelBootstrap(this); + } + + /** + * Close the {@link QuicChannel} + * + * @param applicationClose {@code true} if an application close should be used, + * {@code false} if a normal close should be used. + * @param error the application error number, or {@code 0} if no special error should be signaled. + * @param reason the reason for the closure (which may be an empty {@link ByteBuf}. + * @return the future that is notified. + */ + default ChannelFuture close(boolean applicationClose, int error, ByteBuf reason) { + return close(applicationClose, error, reason, newPromise()); + } + + /** + * Close the {@link QuicChannel} + * + * @param applicationClose {@code true} if an application close should be used, + * {@code false} if a normal close should be used. + * @param error the application error number, or {@code 0} if no special error should be signaled. + * @param reason the reason for the closure (which may be an empty {@link ByteBuf}. + * @param promise the {@link ChannelPromise} that will be notified. + * @return the future that is notified. + */ + ChannelFuture close(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise); + + /** + * Collects statistics about the connection and notifies the {@link Future} once done. + * + * @return the {@link Future} that is notified once the stats were collected. + */ + default Future collectStats() { + return collectStats(eventLoop().newPromise()); + } + + /** + * Collects statistics about the connection and notifies the {@link Promise} once done. + * + * @param promise the {@link ChannelPromise} that is notified once the stats were collected. + * @return the {@link Future} that is notified once the stats were collected. + */ + Future collectStats(Promise promise); + + /** + * Creates a new {@link QuicChannelBootstrap} that can be used to create and connect new {@link QuicChannel}s to + * endpoints using the given {@link Channel} as transport layer. + * + * @param channel the {@link Channel} that is used as transport layer. + * @return {@link QuicChannelBootstrap} that can be used to bootstrap a client side {@link QuicChannel}. + */ + static QuicChannelBootstrap newBootstrap(Channel channel) { + return new QuicChannelBootstrap(channel); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelBootstrap.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelBootstrap.java new file mode 100644 index 0000000..6976491 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelBootstrap.java @@ -0,0 +1,244 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoop; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Bootstrap that helps to bootstrap {@link QuicChannel}s and connecting these to remote peers. + */ +public final class QuicChannelBootstrap { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicChannelBootstrap.class); + + private final Channel parent; + // The order in which ChannelOptions are applied is important they may depend on each other for validation + // purposes. + private final Map, Object> options = new LinkedHashMap<>(); + private final Map, Object> attrs = new HashMap<>(); + private final Map, Object> streamOptions = new LinkedHashMap<>(); + private final Map, Object> streamAttrs = new HashMap<>(); + + private SocketAddress local; + private SocketAddress remote; + private QuicConnectionAddress connectionAddress = QuicConnectionAddress.EPHEMERAL; + private ChannelHandler handler; + private ChannelHandler streamHandler; + + /** + * Creates a new instance which uses the given {@link Channel} to bootstrap the {@link QuicChannel}. + * This {@link io.netty.channel.ChannelPipeline} of the {@link Channel} needs to have the quic codec in the + * pipeline. + * + * @param parent the {@link Channel} that is used as the transport layer. + */ + QuicChannelBootstrap(Channel parent) { + Quic.ensureAvailability(); + this.parent = ObjectUtil.checkNotNull(parent, "parent"); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicChannel}. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap option(ChannelOption option, T value) { + Quic.updateOptions(options, option, value); + return this; + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicChannel}. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap attr(AttributeKey key, T value) { + Quic.updateAttributes(attrs, key, value); + return this; + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicChannel} once created. + * + * @param handler the {@link ChannelHandler} that is added to the {@link QuicChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicChannelBootstrap handler(ChannelHandler handler) { + this.handler = ObjectUtil.checkNotNull(handler, "handler"); + return this; + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap streamOption(ChannelOption option, T value) { + Quic.updateOptions(streamOptions, option, value); + return this; + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap streamAttr(AttributeKey key, T value) { + Quic.updateAttributes(streamAttrs, key, value); + return this; + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} once created. + * + * @param streamHandler the {@link ChannelHandler} that is added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicChannelBootstrap streamHandler(ChannelHandler streamHandler) { + this.streamHandler = ObjectUtil.checkNotNull(streamHandler, "streamHandler"); + return this; + } + + /** + * Set the local address. + * + * @param local the {@link SocketAddress} of the local peer. + * @return this instance. + */ + public QuicChannelBootstrap localAddress(SocketAddress local) { + this.local = ObjectUtil.checkNotNull(local, "local"); + return this; + } + + /** + * Set the remote address of the host to talk to. + * + * @param remote the {@link SocketAddress} of the remote peer. + * @return this instance. + */ + public QuicChannelBootstrap remoteAddress(SocketAddress remote) { + this.remote = ObjectUtil.checkNotNull(remote, "remote"); + return this; + } + + /** + * Set the {@link QuicConnectionAddress} to use. If none is specified a random address is generated on your + * behalf. + * + * @param connectionAddress the {@link QuicConnectionAddress} to use. + * @return this instance. + */ + public QuicChannelBootstrap connectionAddress(QuicConnectionAddress connectionAddress) { + this.connectionAddress = ObjectUtil.checkNotNull(connectionAddress, "connectionAddress"); + return this; + } + + /** + * Connects a {@link QuicChannel} to the remote peer and notifies the future once done. + * + * @return {@link Future} which is notified once the operation completes. + */ + public Future connect() { + return connect(parent.eventLoop().newPromise()); + } + + /** + * Connects a {@link QuicChannel} to the remote peer and notifies the promise once done. + * + * @param promise the {@link Promise} which is notified once the operations completes. + * @return {@link Future} which is notified once the operation completes. + + */ + public Future connect(Promise promise) { + if (handler == null && streamHandler == null) { + throw new IllegalStateException("handler and streamHandler not set"); + } + SocketAddress local = this.local; + if (local == null) { + local = parent.localAddress(); + } + if (local == null) { + local = new InetSocketAddress(0); + } + + SocketAddress remote = this.remote; + if (remote == null) { + remote = parent.remoteAddress(); + } + if (remote == null) { + throw new IllegalStateException("remote not set"); + } + + final QuicConnectionAddress address = connectionAddress; + QuicChannel channel = QuicheQuicChannel.forClient(parent, (InetSocketAddress) local, + (InetSocketAddress) remote, + streamHandler, Quic.toOptionsArray(streamOptions), Quic.toAttributesArray(streamAttrs)); + + Quic.setupChannel(channel, Quic.toOptionsArray(options), Quic.toAttributesArray(attrs), handler, logger); + EventLoop eventLoop = parent.eventLoop(); + eventLoop.register(channel).addListener((ChannelFuture future) -> { + Throwable cause = future.cause(); + if (cause != null) { + promise.setFailure(cause); + } else { + channel.connect(address).addListener(f -> { + Throwable error = f.cause(); + if (error != null) { + promise.setFailure(error); + } else { + promise.setSuccess(channel); + } + }); + } + }); + return promise; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelConfig.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelConfig.java new file mode 100644 index 0000000..9e3336f --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelConfig; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; + +/** + * A QUIC {@link ChannelConfig}. + */ +public interface QuicChannelConfig extends ChannelConfig { + + @Override + @Deprecated + QuicChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead); + + @Override + QuicChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis); + + @Override + QuicChannelConfig setWriteSpinCount(int writeSpinCount); + + @Override + QuicChannelConfig setAllocator(ByteBufAllocator allocator); + + @Override + QuicChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator); + + @Override + QuicChannelConfig setAutoRead(boolean autoRead); + + @Override + QuicChannelConfig setAutoClose(boolean autoClose); + + @Override + QuicChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark); + + @Override + QuicChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark); + + @Override + QuicChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark); + + @Override + QuicChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelOption.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelOption.java new file mode 100644 index 0000000..41ebe4d --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelOption.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.ChannelOption; + +/** + * {@link ChannelOption}s specific to QUIC. + */ +public final class QuicChannelOption extends ChannelOption { + + /** + * If set to {@code true} the {@link QuicStreamChannel} will read {@link QuicStreamFrame}s and fire it through + * the pipeline, if {@code false} it will read {@link io.netty.buffer.ByteBuf} and translate the FIN flag to + * events. + */ + public static final ChannelOption READ_FRAMES = + valueOf(QuicChannelOption.class, "READ_FRAMES"); + + /** + * Enable qlog + * for a {@link QuicChannel}. + */ + public static final ChannelOption QLOG = valueOf(QuicChannelOption.class, "QLOG"); + + /** + * Use GSO + * for QUIC packets if possible. + */ + public static final ChannelOption SEGMENTED_DATAGRAM_PACKET_ALLOCATOR = + valueOf(QuicChannelOption.class, "SEGMENTED_DATAGRAM_PACKET_ALLOCATOR"); + + @SuppressWarnings({ "deprecation" }) + private QuicChannelOption() { + super(null); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClientCodecBuilder.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClientCodecBuilder.java new file mode 100644 index 0000000..89df278 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClientCodecBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.ChannelHandler; + +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * {@link QuicCodecBuilder} that configures and builds a {@link ChannelHandler} that should be added to the + * {@link io.netty.channel.ChannelPipeline} of a {@code QUIC} client. + */ +public final class QuicClientCodecBuilder extends QuicCodecBuilder { + + /** + * Creates a new instance. + */ + public QuicClientCodecBuilder() { + super(false); + } + + private QuicClientCodecBuilder(QuicCodecBuilder builder) { + super(builder); + } + + @Override + public QuicClientCodecBuilder clone() { + return new QuicClientCodecBuilder(this); + } + + @Override + protected ChannelHandler build(QuicheConfig config, + Function sslEngineProvider, + Executor sslTaskExecutor, + int localConnIdLength, FlushStrategy flushStrategy) { + return new QuicheQuicClientCodec(config, sslEngineProvider, sslTaskExecutor, localConnIdLength, flushStrategy); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClientSessionCache.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClientSessionCache.java new file mode 100644 index 0000000..1ff5b91 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClientSessionCache.java @@ -0,0 +1,244 @@ +/* + * 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.handler.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)); + } + } + } + + // Only used for testing. + boolean hasSession(String host, int port) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + synchronized (sessions) { + return sessions.containsKey(hostPort); + } + } + return false; + } + + 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/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClosedChannelException.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClosedChannelException.java new file mode 100644 index 0000000..f665b67 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicClosedChannelException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import java.nio.channels.ClosedChannelException; + +/** + * Special {@link QuicClosedChannelException} which also provides extra info if the close was a result of a + * {@link QuicConnectionCloseEvent} that was triggered by the remote peer. + */ +public final class QuicClosedChannelException extends ClosedChannelException { + + private final QuicConnectionCloseEvent event; + + QuicClosedChannelException(QuicConnectionCloseEvent event) { + this.event = event; + } + + /** + * Returns the {@link QuicConnectionCloseEvent} that caused the closure or {@code null} if none was received. + * + * @return the event. + */ + public QuicConnectionCloseEvent event() { + return event; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicCodecBuilder.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicCodecBuilder.java new file mode 100644 index 0000000..9c6a764 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicCodecBuilder.java @@ -0,0 +1,508 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.ChannelHandler; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static io.netty.util.internal.ObjectUtil.checkInRange; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * Abstract base class for {@code QUIC} codec builders. + * + * @param the type of the {@link QuicCodecBuilder}. + */ +public abstract class QuicCodecBuilder> { + private final boolean server; + private Boolean grease; + private Long maxIdleTimeout; + private Long maxRecvUdpPayloadSize; + private Long maxSendUdpPayloadSize; + private Long initialMaxData; + private Long initialMaxStreamDataBidiLocal; + private Long initialMaxStreamDataBidiRemote; + private Long initialMaxStreamDataUni; + private Long initialMaxStreamsBidi; + private Long initialMaxStreamsUni; + private Long ackDelayExponent; + private Long maxAckDelay; + private Boolean disableActiveMigration; + private Boolean enableHystart; + private QuicCongestionControlAlgorithm congestionControlAlgorithm; + private int localConnIdLength; + private Function sslEngineProvider; + private FlushStrategy flushStrategy = FlushStrategy.DEFAULT; + private Integer recvQueueLen; + private Integer sendQueueLen; + private Long activeConnectionIdLimit; + private byte[] statelessResetToken; + + private Executor sslTaskExecutor; + + // package-private for testing only + int version; + + QuicCodecBuilder(boolean server) { + Quic.ensureAvailability(); + this.version = Quiche.QUICHE_PROTOCOL_VERSION; + this.localConnIdLength = Quiche.QUICHE_MAX_CONN_ID_LEN; + this.server = server; + } + + QuicCodecBuilder(QuicCodecBuilder builder) { + Quic.ensureAvailability(); + this.server = builder.server; + this.grease = builder.grease; + this.maxIdleTimeout = builder.maxIdleTimeout; + this.maxRecvUdpPayloadSize = builder.maxRecvUdpPayloadSize; + this.maxSendUdpPayloadSize = builder.maxSendUdpPayloadSize; + this.initialMaxData = builder.initialMaxData; + this.initialMaxStreamDataBidiLocal = builder.initialMaxStreamDataBidiLocal; + this.initialMaxStreamDataBidiRemote = builder.initialMaxStreamDataBidiRemote; + this.initialMaxStreamDataUni = builder.initialMaxStreamDataUni; + this.initialMaxStreamsBidi = builder.initialMaxStreamsBidi; + this.initialMaxStreamsUni = builder.initialMaxStreamsUni; + this.ackDelayExponent = builder.ackDelayExponent; + this.maxAckDelay = builder.maxAckDelay; + this.disableActiveMigration = builder.disableActiveMigration; + this.enableHystart = builder.enableHystart; + this.congestionControlAlgorithm = builder.congestionControlAlgorithm; + this.localConnIdLength = builder.localConnIdLength; + this.sslEngineProvider = builder.sslEngineProvider; + this.flushStrategy = builder.flushStrategy; + this.recvQueueLen = builder.recvQueueLen; + this.sendQueueLen = builder.sendQueueLen; + this.activeConnectionIdLimit = builder.activeConnectionIdLimit; + this.statelessResetToken = builder.statelessResetToken; + this.sslTaskExecutor = builder.sslTaskExecutor; + this.version = builder.version; + } + + /** + * Returns itself. + * + * @return itself. + */ + @SuppressWarnings("unchecked") + protected final B self() { + return (B) this; + } + + /** + * Sets the {@link FlushStrategy} that will be used to detect when an automatic flush + * should happen. + * + * @param flushStrategy the strategy to use. + * @return the instance itself. + */ + public final B flushStrategy(FlushStrategy flushStrategy) { + this.flushStrategy = Objects.requireNonNull(flushStrategy, "flushStrategy"); + return self(); + } + + /** + * Sets the congestion control algorithm to use. + * + * The default is {@link QuicCongestionControlAlgorithm#CUBIC}. + * + * @param congestionControlAlgorithm the {@link QuicCongestionControlAlgorithm} to use. + * @return the instance itself. + */ + public final B congestionControlAlgorithm(QuicCongestionControlAlgorithm congestionControlAlgorithm) { + this.congestionControlAlgorithm = congestionControlAlgorithm; + return self(); + } + + /** + * Set if greasing should be enabled + * or not. + * + * The default value is {@code true}. + * + * @param enable {@code true} if enabled, {@code false} otherwise. + * @return the instance itself. + */ + public final B grease(boolean enable) { + grease = enable; + return self(); + } + + /** + * See + * set_max_idle_timeout. + * + * The default value is infinite, that is, no timeout is used. + * + * @param amount the maximum idle timeout. + * @param unit the {@link TimeUnit}. + * @return the instance itself. + */ + public final B maxIdleTimeout(long amount, TimeUnit unit) { + this.maxIdleTimeout = unit.toMillis(checkPositiveOrZero(amount, "amount")); + return self(); + } + + /** + * See + * set_max_send_udp_payload_size. + * + * The default and minimum value is 1200. + * + * @param size the maximum payload size that is advertised to the remote peer. + * @return the instance itself. + */ + public final B maxSendUdpPayloadSize(long size) { + this.maxSendUdpPayloadSize = checkPositiveOrZero(size, "value"); + return self(); + } + + /** + * See + * set_max_recv_udp_payload_size. + * + * The default value is 65527. + * + * @param size the maximum payload size that is advertised to the remote peer. + * @return the instance itself. + */ + public final B maxRecvUdpPayloadSize(long size) { + this.maxRecvUdpPayloadSize = checkPositiveOrZero(size, "value"); + return self(); + } + + /** + * See + * set_initial_max_data. + * + * The default value is 0. + * + * @param value the initial maximum data limit. + * @return the instance itself. + */ + public final B initialMaxData(long value) { + this.initialMaxData = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_stream_data_bidi_local. + * + * The default value is 0. + * + * @param value the initial maximum data limit for local bidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamDataBidirectionalLocal(long value) { + this.initialMaxStreamDataBidiLocal = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_stream_data_bidi_remote. + * + * The default value is 0. + * + * @param value the initial maximum data limit for remote bidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamDataBidirectionalRemote(long value) { + this.initialMaxStreamDataBidiRemote = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_stream_data_uni. + * + * The default value is 0. + * + * @param value the initial maximum data limit for unidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamDataUnidirectional(long value) { + this.initialMaxStreamDataUni = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_streams_bidi. + * + * The default value is 0. + * + * @param value the initial maximum stream limit for bidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamsBidirectional(long value) { + this.initialMaxStreamsBidi = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_streams_uni. + * + * The default value is 0. + * + * @param value the initial maximum stream limit for unidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamsUnidirectional(long value) { + this.initialMaxStreamsUni = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_ack_delay_exponent. + * + * The default value is 3. + * + * @param value the delay exponent used for ACKs. + * @return the instance itself. + */ + public final B ackDelayExponent(long value) { + this.ackDelayExponent = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_max_ack_delay. + * + * The default value is 25 milliseconds. + * + * @param amount the max ack delay. + * @param unit the {@link TimeUnit}. + * @return the instance itself. + */ + public final B maxAckDelay(long amount, TimeUnit unit) { + this.maxAckDelay = unit.toMillis(checkPositiveOrZero(amount, "amount")); + return self(); + } + + /** + * See + * + * set_disable_active_migration. + * + * The default value is {@code true}. + * + * @param enable {@code true} if migration should be enabled, {@code false} otherwise. + * @return the instance itself. + */ + public final B activeMigration(boolean enable) { + this.disableActiveMigration = !enable; + return self(); + } + + /** + * See + * + * enable_hystart. + * + * The default value is {@code true}. + * + * @param enable {@code true} if Hystart should be enabled. + * @return the instance itself. + */ + public final B hystart(boolean enable) { + this.enableHystart = enable; + return self(); + } + + /** + * Sets the local connection id length that is used. + * + * The default is 20, which is also the maximum that is supported. + * + * @param value the length of local generated connections ids. + * @return the instance itself. + */ + public final B localConnectionIdLength(int value) { + this.localConnIdLength = checkInRange(value, 0, Quiche.QUICHE_MAX_CONN_ID_LEN, "value"); + return self(); + } + + /** + * Allows to configure the {@code QUIC version} that should be used. + * + * The default value is the latest supported version by the underlying library. + * + * @param version the {@code QUIC version} to use. + * @return the instance itself. + */ + public final B version(int version) { + this.version = version; + return self(); + } + + /** + * If configured this will enable + * Datagram support. + * @param recvQueueLen the RECV queue length. + * @param sendQueueLen the SEND queue length. + * @return the instance itself. + */ + public final B datagram(int recvQueueLen, int sendQueueLen) { + checkPositive(recvQueueLen, "recvQueueLen"); + checkPositive(sendQueueLen, "sendQueueLen"); + + this.recvQueueLen = recvQueueLen; + this.sendQueueLen = sendQueueLen; + return self(); + } + + /** + * The {@link QuicSslContext} that will be used to create {@link QuicSslEngine}s for {@link QuicChannel}s. + * + * If you need a more flexible way to provide {@link QuicSslEngine}s use {@link #sslEngineProvider(Function)}. + * + * @param sslContext the context. + * @return the instance itself. + */ + public final B sslContext(QuicSslContext sslContext) { + if (server != sslContext.isServer()) { + throw new IllegalArgumentException("QuicSslContext.isServer() " + sslContext.isServer() + + " isn't supported by this builder"); + } + return sslEngineProvider(q -> sslContext.newEngine(q.alloc())); + } + + /** + * The {@link Function} that will return the {@link QuicSslEngine} that should be used for the + * {@link QuicChannel}. + * + * @param sslEngineProvider the provider. + * @return the instance itself. + */ + public final B sslEngineProvider(Function sslEngineProvider) { + this.sslEngineProvider = sslEngineProvider; + return self(); + } + + /** + * Allow to configure a {@link Executor} that will be used to run expensive SSL operations. + * + * @param sslTaskExecutor the {@link Executor} that will be used to offload expensive SSL operations. + * @return the instance itself. + */ + public final B sslTaskExecutor(Executor sslTaskExecutor) { + this.sslTaskExecutor = sslTaskExecutor; + return self(); + } + + /** + * Allows to configure the {@code active connect id limit} that should be used. + * + * @param limit the limit to use. + * @return the instance itself. + */ + public final B activeConnectionIdLimit(long limit) { + checkPositive(limit, "limit"); + + this.activeConnectionIdLimit = limit; + return self(); + } + + + /** + * Allows to configure the {@code active connect id limit} that should be used. + * + * @param token the token to use. + * @return the instance itself. + */ + public final B statelessResetToken(byte[] token) { + if (token.length != 16) { + throw new IllegalArgumentException("token must be 16 bytes but was " + token.length); + } + + this.statelessResetToken = token.clone(); + return self(); + } + + private QuicheConfig createConfig() { + return new QuicheConfig(version, grease, + maxIdleTimeout, maxSendUdpPayloadSize, maxRecvUdpPayloadSize, initialMaxData, + initialMaxStreamDataBidiLocal, initialMaxStreamDataBidiRemote, + initialMaxStreamDataUni, initialMaxStreamsBidi, initialMaxStreamsUni, + ackDelayExponent, maxAckDelay, disableActiveMigration, enableHystart, + congestionControlAlgorithm, recvQueueLen, sendQueueLen, activeConnectionIdLimit, statelessResetToken); + } + + /** + * Validate the configuration before building the codec. + */ + protected void validate() { + if (sslEngineProvider == null) { + throw new IllegalStateException("sslEngineProvider can't be null"); + } + } + + /** + * Builds the QUIC codec that should be added to the {@link io.netty.channel.ChannelPipeline} of the underlying + * {@link io.netty.channel.Channel} which is used as transport for QUIC. + * + * @return the {@link ChannelHandler} which acts as QUIC codec. + */ + public final ChannelHandler build() { + validate(); + QuicheConfig config = createConfig(); + try { + return build(config, sslEngineProvider, sslTaskExecutor, localConnIdLength, flushStrategy); + } catch (Throwable cause) { + config.free(); + throw cause; + } + } + + /** + * Clone the builder + * + * @return the new instance that is a clone if this instance. + */ + public abstract B clone(); + + /** + * Builds the QUIC codec. + * + * @param config the {@link QuicheConfig} that should be used. + * @param sslContextProvider the context provider + * @param sslTaskExecutor the {@link Executor} to use. + * @param localConnIdLength the local connection id length. + * @param flushStrategy the {@link FlushStrategy} that should be used. + * @return the {@link ChannelHandler} which acts as codec. + */ + protected abstract ChannelHandler build(QuicheConfig config, + Function sslContextProvider, + Executor sslTaskExecutor, + int localConnIdLength, FlushStrategy flushStrategy); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicCongestionControlAlgorithm.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicCongestionControlAlgorithm.java new file mode 100644 index 0000000..2a4d41c --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicCongestionControlAlgorithm.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +/** + * Available congestion control algorithms to use. + */ +public enum QuicCongestionControlAlgorithm { + RENO, + CUBIC, + BBR +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionAddress.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionAddress.java new file mode 100644 index 0000000..8cdbc31 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionAddress.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * A {@link QuicConnectionAddress} that can be used to connect too. + */ +public final class QuicConnectionAddress extends SocketAddress { + + /** + * Special {@link QuicConnectionAddress} that should be used when the connection address should be generated + * and chosen on the fly. + */ + public static final QuicConnectionAddress EPHEMERAL = new QuicConnectionAddress(null, false); + + private final String toStr; + + // Accessed by QuicheQuicheChannel + final ByteBuffer connId; + + /** + * Create a new instance + * + * @param connId the connection id to use. + */ + public QuicConnectionAddress(byte[] connId) { + this(ByteBuffer.wrap(connId.clone()), true); + } + + /** + * Create a new instance + * + * @param connId the connection id to use. + */ + public QuicConnectionAddress(ByteBuffer connId) { + this(connId, true); + } + + private QuicConnectionAddress(ByteBuffer connId, boolean validate) { + Quic.ensureAvailability(); + if (validate && connId.remaining() > Quiche.QUICHE_MAX_CONN_ID_LEN) { + throw new IllegalArgumentException("Connection ID can only be of max length " + + Quiche.QUICHE_MAX_CONN_ID_LEN); + } + this.connId = connId; + if (connId == null) { + toStr = "QuicConnectionAddress{EPHEMERAL}"; + } else { + ByteBuf buffer = Unpooled.wrappedBuffer(connId); + try { + toStr = "QuicConnectionAddress{" + + "connId=" + ByteBufUtil.hexDump(buffer) + '}'; + } finally { + buffer.release(); + } + } + } + + @Override + public String toString() { + return toStr; + } + + @Override + public int hashCode() { + if (this == EPHEMERAL) { + return System.identityHashCode(EPHEMERAL); + } + return Objects.hash(connId); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof QuicConnectionAddress)) { + return false; + } + QuicConnectionAddress address = (QuicConnectionAddress) obj; + if (obj == this) { + return true; + } + if (connId == null) { + return false; + } + return connId.equals(address.connId); + } + + /** + * Return a random generated {@link QuicConnectionAddress} of a given length + * that can be used to connect a {@link QuicChannel} + * + * @param length the length of the {@link QuicConnectionAddress} to generate. + * @return the generated address. + */ + public static QuicConnectionAddress random(int length) { + return new QuicConnectionAddress(QuicConnectionIdGenerator.randomGenerator().newId(length)); + } + + /** + * Return a random generated {@link QuicConnectionAddress} of maximum size + * that can be used to connect a {@link QuicChannel} + * + * @return the generated address. + */ + public static QuicConnectionAddress random() { + return random(Quiche.QUICHE_MAX_CONN_ID_LEN); + } + +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionCloseEvent.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionCloseEvent.java new file mode 100644 index 0000000..c3a9905 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionCloseEvent.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import java.util.Arrays; + +/** + * Event that is generated if the remote peer sends a + * CLOSE_CONNECTION frame. + * This allows to inspect the various details of the cause of the close. + */ +public final class QuicConnectionCloseEvent implements QuicEvent { + + final boolean applicationClose; + final int error; + final byte[] reason; + + QuicConnectionCloseEvent(boolean applicationClose, int error, byte[] reason) { + this.applicationClose = applicationClose; + this.error = error; + this.reason = reason; + } + + /** + * Return {@code true} if this was an application close, {@code false} otherwise. + * + * @return if this is an application close. + */ + public boolean isApplicationClose() { + return applicationClose; + } + + /** + * Return the error that was provided for the close. + * + * @return the error. + */ + public int error() { + return error; + } + + /** + * Returns {@code true} if a TLS error + * is contained. + * @return {@code true} if this is an {@code TLS error}, {@code false} otherwise. + */ + public boolean isTlsError() { + return !applicationClose && error >= 0x0100; + } + + /** + * Returns the reason for the close, which may be empty if no reason was given as part of the close. + * + * @return the reason. + */ + public byte[] reason() { + return reason.clone(); + } + + @Override + public String toString() { + return "QuicConnectionCloseEvent{" + + "applicationClose=" + applicationClose + + ", error=" + error + + ", reason=" + Arrays.toString(reason) + + '}'; + } + + /** + * Extract the contained {@code TLS error} from the {@code QUIC error}. If the given {@code QUIC error} does not + * contain a {@code TLS error} it will return {@code -1}. + * + * @param error the {@code QUIC error} + * @return the {@code TLS error} or {@code -1} if there was no {@code TLS error} contained. + */ + public static int extractTlsError(int error) { + int tlsError = error - 0x0100; + if (tlsError < 0) { + return -1; + } + return tlsError; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionIdGenerator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionIdGenerator.java new file mode 100644 index 0000000..fa62816 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionIdGenerator.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import java.nio.ByteBuffer; + +/** + * Creates new connection id instances. + */ +public interface QuicConnectionIdGenerator { + /** + * Creates a new connection id with the given length. This method may not be supported by + * a sign id generator implementation as a sign id generator should always have an input + * to sign with, otherwise this method may generate the same id which may cause some + * unpredictable issues when we use it. + * + * @param length the length of the id. + * @return the id. + */ + ByteBuffer newId(int length); + + /** + * Creates a new connection id with the given length. The given input may be used to sign or + * seed the id, or may be ignored (depending on the implementation). + * + * @param input the input which may be used to generate the id. + * @param length the length of the id. + * @return the id. + */ + ByteBuffer newId(ByteBuffer input, int length); + + /** + * Returns the maximum length of a connection id. + * + * @return the maximum length of a connection id that is supported. + */ + int maxConnectionIdLength(); + + /** + * Returns true if the implementation is idempotent, which means we will get the same id + * with the same input ByteBuffer. Otherwise, returns false. + * + * @return whether the implementation is idempotent. + */ + boolean isIdempotent(); + + /** + * Return a {@link QuicConnectionIdGenerator} which randomly generates new connection ids. + * + * @return a {@link QuicConnectionIdGenerator} which randomly generated ids. + */ + static QuicConnectionIdGenerator randomGenerator() { + return SecureRandomQuicConnectionIdGenerator.INSTANCE; + } + + /** + * Return a {@link QuicConnectionIdGenerator} which generates new connection ids by signing the given input. + * + * @return a {@link QuicConnectionIdGenerator} which generates ids by signing the given input. + */ + static QuicConnectionIdGenerator signGenerator() { + return HmacSignQuicConnectionIdGenerator.INSTANCE; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionStats.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionStats.java new file mode 100644 index 0000000..027d7e9 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionStats.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +/** + * Statistics about the {@code QUIC} connection. If unknown by the implementation it might return {@code -1} values + * for the various methods. + */ +public interface QuicConnectionStats { + /** + * @return The number of QUIC packets received on the connection. + */ + long recv(); + + /** + * @return The number of QUIC packets sent on this connection. + */ + long sent(); + + /** + * @return The number of QUIC packets that were lost. + */ + long lost(); + + /** + * @return The number of sent QUIC packets with retransmitted data. + */ + long retrans(); + + /** + * @return The number of sent bytes. + */ + long sentBytes(); + + /** + * @return The number of received bytes. + */ + long recvBytes(); + + /** + * @return The number of bytes lost. + */ + long lostBytes(); + + /** + * @return The number of stream bytes retransmitted. + */ + long streamRetransBytes(); + + /** + * @return The number of known paths for the connection. + */ + long pathsCount(); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicDatagramExtensionEvent.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicDatagramExtensionEvent.java new file mode 100644 index 0000000..6ad3300 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicDatagramExtensionEvent.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +/** + * Used when the remote peer supports the + * QUIC DATAGRAM extension. + */ +public final class QuicDatagramExtensionEvent implements QuicExtensionEvent { + + private final int maxLength; + + QuicDatagramExtensionEvent(int maxLength) { + this.maxLength = ObjectUtil.checkPositiveOrZero(maxLength, "maxLength"); + } + + /** + * The maximum datagram payload length the peer will accept. If you try to write bigger datagrams the write will + * fail. + * + * @return the max length. + */ + public int maxLength() { + return maxLength; + } + + @Override + public String toString() { + return "QuicDatagramExtensionEvent{" + + "maxLength=" + maxLength + + '}'; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicError.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicError.java new file mode 100644 index 0000000..0e8aec4 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicError.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; + +/** + * All QUIC error codes identified by Quiche. + * See Error + */ +public enum QuicError { + BUFFER_TOO_SHORT(Quiche.QUICHE_ERR_BUFFER_TOO_SHORT, "QUICHE_ERR_BUFFER_TOO_SHORT"), + UNKNOWN_VERSION(Quiche.QUICHE_ERR_UNKNOWN_VERSION, "QUICHE_ERR_UNKNOWN_VERSION"), + INVALID_FRAME(Quiche.QUICHE_ERR_INVALID_FRAME, "QUICHE_ERR_INVALID_FRAME"), + INVALID_PACKET(Quiche.QUICHE_ERR_INVALID_PACKET, "QUICHE_ERR_INVALID_PACKET"), + INVALID_STATE(Quiche.QUICHE_ERR_INVALID_STATE, "QUICHE_ERR_INVALID_STATE"), + INVALID_STREAM_STATE(Quiche.QUICHE_ERR_INVALID_STREAM_STATE, "QUICHE_ERR_INVALID_STREAM_STATE"), + INVALID_TRANSPORT_PARAM(Quiche.QUICHE_ERR_INVALID_TRANSPORT_PARAM, "QUICHE_ERR_INVALID_TRANSPORT_PARAM"), + CRYPTO_FAIL(Quiche.QUICHE_ERR_CRYPTO_FAIL, "QUICHE_ERR_CRYPTO_FAIL"), + TLS_FAIL(Quiche.QUICHE_ERR_TLS_FAIL, "QUICHE_ERR_TLS_FAIL"), + FLOW_CONTROL(Quiche.QUICHE_ERR_FLOW_CONTROL, "QUICHE_ERR_FLOW_CONTROL"), + STREAM_LIMIT(Quiche.QUICHE_ERR_STREAM_LIMIT, "QUICHE_ERR_STREAM_LIMIT"), + FINAL_SIZE(Quiche.QUICHE_ERR_FINAL_SIZE, "QUICHE_ERR_FINAL_SIZE"), + CONGESTION_CONTROL(Quiche.QUICHE_ERR_CONGESTION_CONTROL, "QUICHE_ERR_CONGESTION_CONTROL"), + STREAM_RESET(Quiche.QUICHE_ERR_STREAM_RESET, "STREAM_RESET"), + STREAM_STOPPED(Quiche.QUICHE_ERR_STREAM_STOPPED, "STREAM_STOPPED"), + ID_LIMIT(Quiche.QUICHE_ERR_ID_LIMIT, "ID_LIMIT"), + QUT_OF_IDENTIFIERS(Quiche.QUICHE_ERR_OUT_OF_IDENTIFIERS, "OUT_OF_IDENTIFIERS"), + KEY_UPDATE(Quiche.QUICHE_ERR_KEY_UPDATE, "KEY_UPDATE"); + + private static final IntObjectMap ERROR_MAP = new IntObjectHashMap<>(); + + static { + for (QuicError errorCode : QuicError.values()) { + ERROR_MAP.put(errorCode.code(), errorCode); + } + } + + private final int code; + private final String message; + + QuicError(int code, String message) { + this.code = code; + this.message = message; + } + + final int code() { + return code; + } + + final String message() { + return message; + } + + @Override + public final String toString() { + return String.format("QuicError{code=%d, message=%s}", code, message); + } + + static QuicError valueOf(int code) { + final QuicError errorCode = ERROR_MAP.get(code); + if (errorCode == null) { + throw new IllegalArgumentException("unknown " + QuicError.class.getSimpleName() + " code: " + code); + } + return errorCode; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicEvent.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicEvent.java new file mode 100644 index 0000000..26cbc61 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicEvent.java @@ -0,0 +1,23 @@ +/* + * 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.handler.codec.quic; + +/** + * Marker interface for events that will be passed through the {@link io.netty.channel.ChannelPipeline} via + * {@link io.netty.channel.ChannelPipeline#fireUserEventTriggered(Object)} to notify the user about {@code QUIC} + * specific events. + */ +public interface QuicEvent { } diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicException.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicException.java new file mode 100644 index 0000000..90534e3 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import java.io.IOException; + +/** + * Exception produced while processing {@code QUIC}. + */ +public final class QuicException extends IOException { + + private final QuicError error; + + QuicException(QuicError error) { + super(error.message()); + this.error = error; + } + + /** + * Returns the {@link QuicError} which was the cause of the {@link QuicException}. + * + * @return the {@link QuicError} that caused this {@link QuicException}. + */ + public QuicError error() { + return error; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicExtensionEvent.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicExtensionEvent.java new file mode 100644 index 0000000..1112bb4 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicExtensionEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +/** + * Marker interface for events that will be passed through the {@link io.netty.channel.ChannelPipeline} via + * {@link io.netty.channel.ChannelPipeline#fireUserEventTriggered(Object)} to notify the user about supported + * QUIC extensions by the remote peer. + */ +public interface QuicExtensionEvent extends QuicEvent { } diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicHeaderParser.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicHeaderParser.java new file mode 100644 index 0000000..f24fa24 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicHeaderParser.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.net.InetSocketAddress; + +import static io.netty.handler.codec.quic.Quiche.allocateNativeOrder; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * Parses the QUIC packet header and notifies a callback once parsing was successful. + * + * Once the parser is not needed anymore the user needs to call {@link #close()} to ensure all resources are + * released. Failed to do so may lead to memory leaks. + * + * This class can be used for advanced use-cases. Usually you want to just use {@link QuicClientCodecBuilder} or + * {@link QuicServerCodecBuilder}. + */ +public final class QuicHeaderParser implements AutoCloseable { + private final int maxTokenLength; + private final int localConnectionIdLength; + private final ByteBuf versionBuffer; + private final ByteBuf typeBuffer; + private final ByteBuf scidLenBuffer; + private final ByteBuf scidBuffer; + private final ByteBuf dcidLenBuffer; + private final ByteBuf dcidBuffer; + private final ByteBuf tokenBuffer; + private final ByteBuf tokenLenBuffer; + private boolean closed; + + public QuicHeaderParser(int maxTokenLength, int localConnectionIdLength) { + Quic.ensureAvailability(); + this.maxTokenLength = checkPositiveOrZero(maxTokenLength, "maxTokenLength"); + this.localConnectionIdLength = checkPositiveOrZero(localConnectionIdLength, "localConnectionIdLength"); + // Allocate the buffer from which we read primative values like integer/long with native order to ensure + // we read the right value. + versionBuffer = allocateNativeOrder(Integer.BYTES); + typeBuffer = allocateNativeOrder(Byte.BYTES); + scidLenBuffer = allocateNativeOrder(Integer.BYTES); + dcidLenBuffer = allocateNativeOrder(Integer.BYTES); + tokenLenBuffer = allocateNativeOrder(Integer.BYTES); + + // Now allocate the buffers that dont need native ordering and so will be cheaper to access when we slice into + // these or obtain a view into these via internalNioBuffer(...). + scidBuffer = Unpooled.directBuffer(Quiche.QUICHE_MAX_CONN_ID_LEN); + dcidBuffer = Unpooled.directBuffer(Quiche.QUICHE_MAX_CONN_ID_LEN); + tokenBuffer = Unpooled.directBuffer(maxTokenLength); + } + + @Override + public void close() { + if (!closed) { + closed = true; + versionBuffer.release(); + typeBuffer.release(); + scidBuffer.release(); + scidLenBuffer.release(); + dcidBuffer.release(); + dcidLenBuffer.release(); + tokenLenBuffer.release(); + tokenBuffer.release(); + } + } + + /** + * Parses a QUIC packet and extract the header values out of it. This method takes no ownership of the packet itself + * which means the caller of this method is expected to call {@link ByteBuf#release()} once the packet is not needed + * anymore. + * + * @param sender the sender of the packet. This is directly passed to the {@link QuicHeaderProcessor} once + * parsing was successful. + * @param recipient the recipient of the packet.This is directly passed to the {@link QuicHeaderProcessor} once + * parsing was successful. + * @param packet raw QUIC packet itself. The ownership of the packet is not transferred. This is directly + * passed to the {@link QuicHeaderProcessor} once parsing was successful. + * @param callback the {@link QuicHeaderProcessor} that is called once a QUIC packet could be parsed and all + * the header values be extracted. + * @throws Exception thrown if we couldn't parse the header or if the {@link QuicHeaderProcessor} throws an + * exception. + */ + public void parse(InetSocketAddress sender, + InetSocketAddress recipient, ByteBuf packet, QuicHeaderProcessor callback) throws Exception { + if (closed) { + throw new IllegalStateException(QuicHeaderParser.class.getSimpleName() + " is already closed"); + } + + // Set various len values so quiche_header_info can make use of these. + scidLenBuffer.setInt(0, Quiche.QUICHE_MAX_CONN_ID_LEN); + dcidLenBuffer.setInt(0, Quiche.QUICHE_MAX_CONN_ID_LEN); + tokenLenBuffer.setInt(0, maxTokenLength); + + int res = Quiche.quiche_header_info( + Quiche.readerMemoryAddress(packet), packet.readableBytes(), + localConnectionIdLength, + Quiche.memoryAddress(versionBuffer, 0, versionBuffer.capacity()), + Quiche.memoryAddress(typeBuffer, 0, typeBuffer.capacity()), + Quiche.memoryAddress(scidBuffer, 0, scidBuffer.capacity()), + Quiche.memoryAddress(scidLenBuffer, 0, scidLenBuffer.capacity()), + Quiche.memoryAddress(dcidBuffer, 0, dcidBuffer.capacity()), + Quiche.memoryAddress(dcidLenBuffer, 0, dcidLenBuffer.capacity()), + Quiche.memoryAddress(tokenBuffer, 0, tokenBuffer.capacity()), + Quiche.writerMemoryAddress(tokenLenBuffer)); + if (res >= 0) { + int version = versionBuffer.getInt(0); + byte type = typeBuffer.getByte(0); + int scidLen = scidLenBuffer.getInt(0); + int dcidLen = dcidLenBuffer.getInt(0); + int tokenLen = tokenLenBuffer.getInt(0); + + callback.process(sender, recipient, packet, QuicPacketType.of(type), version, + scidBuffer.setIndex(0, scidLen), + dcidBuffer.setIndex(0, dcidLen), + tokenBuffer.setIndex(0, tokenLen)); + } else { + throw Quiche.newException(res); + } + } + + /** + * Called when a QUIC packet and its header could be parsed. + */ + public interface QuicHeaderProcessor { + + /** + * Called when a QUIC packet header was parsed. + * + * @param sender the sender of the QUIC packet. + * @param recipient the recipient of the QUIC packet. + * @param packet the raw QUIC packet. The ownership is not transferred, which means you will need to call + * {@link ByteBuf#retain()} on it if you want to keep a reference after this method + * returns. + * @param type the type of the packet. + * @param version the version of the packet. + * @param scid the source connection id. The ownership is not transferred and its generally not allowed + * to hold any references to this buffer outside of the method as it will be re-used. + * @param dcid the destination connection id. The ownership is not transferred and its generally not + * allowed to hold any references to this buffer outside of the method as it will be + * re-used. + * @param token the token.The ownership is not transferred and its generally not allowed + * to hold any references to this buffer outside of the method as it will be re-used. + * @throws Exception throws if an error happens during processing. + */ + void process(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf packet, + QuicPacketType type, int version, ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicPacketType.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicPacketType.java new file mode 100644 index 0000000..318c617 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicPacketType.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +/** + * The type of the + * QUIC packet. + */ +public enum QuicPacketType { + /** + * Initial packet. + */ + INITIAL((byte) 1), + + /** + * Retry packet. + */ + RETRY((byte) 2), + + /** + * Handshake packet. + */ + HANDSHAKE((byte) 3), + + /** + * 0-RTT packet. + */ + ZERO_RTT((byte) 4), + + /** + * 1-RTT short header packet. + */ + SHORT((byte) 5), + + /** + * Version negotiation packet. + */ + VERSION_NEGOTIATION((byte) 6); + + final byte type; + + QuicPacketType(byte type) { + this.type = type; + } + + /** + * Return the {@link QuicPacketType} for the given byte. + * + * @param type the byte that represent the type. + * @return the {@link QuicPacketType}. + */ + static QuicPacketType of(byte type) { + switch(type) { + case 1: + return INITIAL; + case 2: + return RETRY; + case 3: + return HANDSHAKE; + case 4: + return ZERO_RTT; + case 5: + return SHORT; + case 6: + return VERSION_NEGOTIATION; + default: + throw new IllegalArgumentException("Unknown QUIC packet type: " + type); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicPathEvent.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicPathEvent.java new file mode 100644 index 0000000..96f6952 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicPathEvent.java @@ -0,0 +1,297 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import java.net.InetSocketAddress; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * A network path specific {@link QuicEvent}. + */ +public abstract class QuicPathEvent implements QuicEvent { + + private final InetSocketAddress local; + private final InetSocketAddress remote; + + QuicPathEvent(InetSocketAddress local, InetSocketAddress remote) { + this.local = requireNonNull(local, "local"); + this.remote = requireNonNull(remote, "remote"); + } + + /** + * The local address of the network path. + * + * @return local + */ + public InetSocketAddress local() { + return local; + } + + /** + * The remote address of the network path. + * + * @return local + */ + public InetSocketAddress remote() { + return remote; + } + + @Override + public String toString() { + return "QuicPathEvent{" + + "local=" + local + + ", remote=" + remote + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + QuicPathEvent that = (QuicPathEvent) o; + if (!Objects.equals(local, that.local)) { + return false; + } + return Objects.equals(remote, that.remote); + } + + @Override + public int hashCode() { + int result = local != null ? local.hashCode() : 0; + result = 31 * result + (remote != null ? remote.hashCode() : 0); + return result; + } + + public static final class New extends QuicPathEvent { + /** + * A new network path (local address, remote address) has been seen on a received packet. + * Note that this event is only triggered for servers, as the client is responsible from initiating new paths. + * The application may then probe this new path, if desired. + * + * @param local local address. + * @param remote remote address. + */ + public New(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.New{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class Validated extends QuicPathEvent { + /** + * The related network path between local and remote has been validated. + * + * @param local local address. + * @param remote remote address. + */ + public Validated(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.Validated{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class FailedValidation extends QuicPathEvent { + /** + * The related network path between local and remote failed to be validated. + * This network path will not be used anymore, unless the application requests probing this path again. + * + * @param local local address. + * @param remote remote address. + */ + public FailedValidation(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.FailedValidation{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class Closed extends QuicPathEvent { + + /** + * The related network path between local and remote has been closed and is now unusable on this connection. + * + * @param local local address. + * @param remote remote address. + */ + public Closed(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return super.equals(o); + } + + @Override + public String toString() { + return "QuicPathEvent.Closed{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class ReusedSourceConnectionId extends QuicPathEvent { + private final long seq; + private final InetSocketAddress oldLocal; + private final InetSocketAddress oldRemote; + + /** + * The stack observes that the Source Connection ID with the given sequence number, + * initially used by the peer over the first pair of addresses, is now reused over + * the second pair of addresses. + * + * @param seq sequence number + * @param oldLocal old local address. + * @param oldRemote old remote address. + * @param local local address. + * @param remote remote address. + */ + public ReusedSourceConnectionId(long seq, InetSocketAddress oldLocal, InetSocketAddress oldRemote, + InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + this.seq = seq; + this.oldLocal = requireNonNull(oldLocal, "oldLocal"); + this.oldRemote = requireNonNull(oldRemote, "oldRemote"); + } + + /** + * Source connection id sequence number. + * + * @return sequence number + */ + public long seq() { + return seq; + } + + /** + * The old local address of the network path. + * + * @return local + */ + public InetSocketAddress oldLocal() { + return oldLocal; + } + + /** + * The old remote address of the network path. + * + * @return local + */ + public InetSocketAddress oldRemote() { + return oldRemote; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + ReusedSourceConnectionId that = (ReusedSourceConnectionId) o; + + if (seq != that.seq) { + return false; + } + if (!Objects.equals(oldLocal, that.oldLocal)) { + return false; + } + return Objects.equals(oldRemote, that.oldRemote); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (int) (seq ^ (seq >>> 32)); + result = 31 * result + (oldLocal != null ? oldLocal.hashCode() : 0); + result = 31 * result + (oldRemote != null ? oldRemote.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "QuicPathEvent.ReusedSourceConnectionId{" + + "seq=" + seq + + ", oldLocal=" + oldLocal + + ", oldRemote=" + oldRemote + + ", local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class PeerMigrated extends QuicPathEvent { + + /** + * The connection observed that the remote migrated over the network path denoted by the pair of addresses, + * i.e., non-probing packets have been received on this network path. This is a server side only event. + * Note that this event is only raised if the path has been validated. + * + * @param local local address. + * @param remote remote address. + */ + public PeerMigrated(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.PeerMigrated{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicResetTokenGenerator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicResetTokenGenerator.java new file mode 100644 index 0000000..feb10d4 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicResetTokenGenerator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import java.nio.ByteBuffer; + +/** + * Generate + * + * stateless reset tokens to use. + */ +public interface QuicResetTokenGenerator { + + /** + * Generate a reset token to use for the given connection id. The returned token MUST be of length 16. + * @param cid + * @return + */ + ByteBuffer newResetToken(ByteBuffer cid); + + /** + * Return a {@link QuicResetTokenGenerator} which generates new reset tokens by signing the given input. + * + * @return a {@link QuicResetTokenGenerator} which generates new reset tokens by signing the given input. + */ + static QuicResetTokenGenerator signGenerator() { + return HmacSignQuicResetTokenGenerator.INSTANCE; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicServerCodecBuilder.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicServerCodecBuilder.java new file mode 100644 index 0000000..7d6ec22 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicServerCodecBuilder.java @@ -0,0 +1,221 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import io.netty.util.internal.ObjectUtil; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * {@link QuicCodecBuilder} that configures and builds a {@link ChannelHandler} that should be added to the + * {@link io.netty.channel.ChannelPipeline} of a {@code QUIC} server. + */ +public final class QuicServerCodecBuilder extends QuicCodecBuilder { + // The order in which ChannelOptions are applied is important they may depend on each other for validation + // purposes. + private final Map, Object> options = new LinkedHashMap<>(); + private final Map, Object> attrs = new HashMap<>(); + private final Map, Object> streamOptions = new LinkedHashMap<>(); + private final Map, Object> streamAttrs = new HashMap<>(); + private ChannelHandler handler; + private ChannelHandler streamHandler; + private QuicConnectionIdGenerator connectionIdAddressGenerator; + private QuicTokenHandler tokenHandler; + private QuicResetTokenGenerator resetTokenGenerator; + + /** + * Creates a new instance. + */ + public QuicServerCodecBuilder() { + super(true); + } + + private QuicServerCodecBuilder(QuicServerCodecBuilder builder) { + super(builder); + options.putAll(builder.options); + attrs.putAll(builder.attrs); + streamOptions.putAll(builder.streamOptions); + streamAttrs.putAll(builder.streamAttrs); + handler = builder.handler; + streamHandler = builder.streamHandler; + connectionIdAddressGenerator = builder.connectionIdAddressGenerator; + tokenHandler = builder.tokenHandler; + } + + @Override + public QuicServerCodecBuilder clone() { + return new QuicServerCodecBuilder(this); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicChannel}. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder option(ChannelOption option, T value) { + Quic.updateOptions(options, option, value); + return self(); + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicChannel}. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder attr(AttributeKey key, T value) { + Quic.updateAttributes(attrs, key, value); + return self(); + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicChannel} once created. + * + * @param handler the {@link ChannelHandler} that is added to the {@link QuicChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicServerCodecBuilder handler(ChannelHandler handler) { + this.handler = ObjectUtil.checkNotNull(handler, "handler"); + return self(); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder streamOption(ChannelOption option, T value) { + Quic.updateOptions(streamOptions, option, value); + return self(); + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder streamAttr(AttributeKey key, T value) { + Quic.updateAttributes(streamAttrs, key, value); + return self(); + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} once created. + * + * @param streamHandler the {@link ChannelHandler} that is added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicServerCodecBuilder streamHandler(ChannelHandler streamHandler) { + this.streamHandler = ObjectUtil.checkNotNull(streamHandler, "streamHandler"); + return self(); + } + + /** + * Sets the {@link QuicConnectionIdGenerator} to use. + * + * @param connectionIdAddressGenerator the {@link QuicConnectionIdGenerator} to use. + * @return this instance. + */ + public QuicServerCodecBuilder connectionIdAddressGenerator( + QuicConnectionIdGenerator connectionIdAddressGenerator) { + this.connectionIdAddressGenerator = connectionIdAddressGenerator; + return this; + } + + /** + * Set the {@link QuicTokenHandler} that is used to generate and validate tokens or + * {@code null} if no tokens should be used at all. + * + * @param tokenHandler the {@link QuicTokenHandler} to use. + * @return this instance. + */ + public QuicServerCodecBuilder tokenHandler(QuicTokenHandler tokenHandler) { + this.tokenHandler = tokenHandler; + return self(); + } + + /** + * Set the {@link QuicResetTokenGenerator} that is used to generate stateless reset tokens or + * {@code null} if the default should be used. + * + * @param resetTokenGenerator the {@link QuicResetTokenGenerator} to use. + * @return this instance. + */ + public QuicServerCodecBuilder resetTokenGenerator(QuicResetTokenGenerator resetTokenGenerator) { + this.resetTokenGenerator = resetTokenGenerator; + return self(); + } + + @Override + protected void validate() { + super.validate(); + if (handler == null && streamHandler == null) { + throw new IllegalStateException("handler and streamHandler not set"); + } + } + + @Override + protected ChannelHandler build(QuicheConfig config, + Function sslEngineProvider, + Executor sslTaskExecutor, + int localConnIdLength, FlushStrategy flushStrategy) { + validate(); + QuicTokenHandler tokenHandler = this.tokenHandler; + if (tokenHandler == null) { + tokenHandler = NoQuicTokenHandler.INSTANCE; + } + QuicConnectionIdGenerator generator = connectionIdAddressGenerator; + if (generator == null) { + generator = QuicConnectionIdGenerator.signGenerator(); + } + QuicResetTokenGenerator resetTokenGenerator = this.resetTokenGenerator; + if (resetTokenGenerator == null) { + resetTokenGenerator = QuicResetTokenGenerator.signGenerator(); + } + ChannelHandler handler = this.handler; + ChannelHandler streamHandler = this.streamHandler; + return new QuicheQuicServerCodec(config, localConnIdLength, tokenHandler, generator, resetTokenGenerator, + flushStrategy, sslEngineProvider, sslTaskExecutor, handler, + Quic.toOptionsArray(options), Quic.toAttributesArray(attrs), + streamHandler, Quic.toOptionsArray(streamOptions), Quic.toAttributesArray(streamAttrs)); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContext.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContext.java new file mode 100644 index 0000000..e367771 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContext.java @@ -0,0 +1,43 @@ +/* + * 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.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.SslContext; + +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Special {@link SslContext} that can be used for {@code QUIC}. + */ +public abstract class QuicSslContext extends SslContext { + + @Override + public abstract QuicSslEngine newEngine(ByteBufAllocator alloc); + + @Override + public abstract QuicSslEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort); + + @Override + public abstract QuicSslSessionContext sessionContext(); + + static X509Certificate[] toX509Certificates0(InputStream stream) + throws CertificateException { + return SslContext.toX509Certificates(stream); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java new file mode 100644 index 0000000..7ea7c97 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java @@ -0,0 +1,381 @@ +/* + * 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.handler.codec.quic; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.util.KeyManagerFactoryWrapper; +import io.netty.handler.ssl.util.TrustManagerFactoryWrapper; +import io.netty.util.Mapping; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import java.io.File; +import java.net.Socket; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Builder for configuring a new SslContext for creation. + */ +public final class QuicSslContextBuilder { + + /** + * Special {@link X509ExtendedKeyManager} implementation which will just fail the certificate selection. + * This is used as a "dummy" implementation when SNI is used as we should always select an other + * {@link QuicSslContext} based on the provided hostname. + */ + private static final X509ExtendedKeyManager SNI_KEYMANAGER = new X509ExtendedKeyManager() { + private final X509Certificate[] emptyCerts = new X509Certificate[0]; + private final String[] emptyStrings = new String[0]; + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return emptyStrings; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return emptyStrings; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return emptyCerts; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return null; + } + }; + + /** + * Creates a builder for new client-side {@link QuicSslContext} that can be used for {@code QUIC}. + */ + public static QuicSslContextBuilder forClient() { + return new QuicSslContextBuilder(false); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}. + * + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @param certChainFile an X.509 certificate chain file in PEM format + * @see #keyManager(File, String, File) + */ + public static QuicSslContextBuilder forServer( + File keyFile, String keyPassword, File certChainFile) { + return new QuicSslContextBuilder(true).keyManager(keyFile, keyPassword, certChainFile); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}. + * + * @param key a PKCS#8 private key + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @param certChain the X.509 certificate chain + * @see #keyManager(File, String, File) + */ + public static QuicSslContextBuilder forServer( + PrivateKey key, String keyPassword, X509Certificate... certChain) { + return new QuicSslContextBuilder(true).keyManager(key, keyPassword, certChain); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}. + * + * @param keyManagerFactory non-{@code null} factory for server's private key + * @see #keyManager(KeyManagerFactory, String) + */ + public static QuicSslContextBuilder forServer(KeyManagerFactory keyManagerFactory, String password) { + return new QuicSslContextBuilder(true).keyManager(keyManagerFactory, password); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} with {@link KeyManager} that can be used for + * {@code QUIC}. + * + * @param keyManager non-{@code null} KeyManager for server's private key + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + */ + public static QuicSslContextBuilder forServer(KeyManager keyManager, String keyPassword) { + return new QuicSslContextBuilder(true).keyManager(keyManager, keyPassword); + } + + /** + * Enables support for + * + * SNI on the server side. + * + * @param mapping the {@link Mapping} that is used to map names to the {@link QuicSslContext} to use. + * Usually using {@link io.netty.util.DomainWildcardMappingBuilder} should be used + * to create the {@link Mapping}. + */ + public static QuicSslContext buildForServerWithSni(Mapping mapping) { + return forServer(SNI_KEYMANAGER, null).sni(mapping).build(); + } + + private final boolean forServer; + private TrustManagerFactory trustManagerFactory; + private String keyPassword; + private KeyManagerFactory keyManagerFactory; + private long sessionCacheSize = 20480; + private long sessionTimeout = 300; + private ClientAuth clientAuth = ClientAuth.NONE; + private String[] applicationProtocols; + private Boolean earlyData; + private BoringSSLKeylog keylog; + private Mapping mapping; + + private QuicSslContextBuilder(boolean forServer) { + this.forServer = forServer; + } + + private QuicSslContextBuilder sni(Mapping mapping) { + this.mapping = checkNotNull(mapping, "mapping"); + return this; + } + + /** + * Enable / disable the usage of early data. + */ + public QuicSslContextBuilder earlyData(boolean enabled) { + this.earlyData = enabled; + return this; + } + + /** + * Enable / disable keylog. When enabled, TLS keys are logged to an internal logger named + * "io.netty.incubator.codec.quic.BoringSSLLogginKeylog" with DEBUG level, see + * {@link BoringSSLKeylog} for detail, logging keys are following + * + * NSS Key Log Format. This is intended for debugging use with tools like Wireshark. + */ + public QuicSslContextBuilder keylog(boolean enabled) { + keylog(enabled ? BoringSSLLoggingKeylog.INSTANCE : null); + return this; + } + + /** + * Enable / disable keylog. When enabled, TLS keys are logged to {@link BoringSSLKeylog#logKey(SSLEngine, String)} + * logging keys are following + * + * NSS Key Log Format. This is intended for debugging use with tools like Wireshark. + */ + public QuicSslContextBuilder keylog(BoringSSLKeylog keylog) { + this.keylog = keylog; + return this; + } + + /** + * Trusted certificates for verifying the remote endpoint's certificate. The file should + * contain an X.509 certificate collection in PEM format. {@code null} uses the system default + * which only works with Java 8u261 and later as these versions support TLS1.3, + * see + * JDK 8u261 Update Release Notes + */ + public QuicSslContextBuilder trustManager(File trustCertCollectionFile) { + try { + return trustManager(QuicheQuicSslContext.toX509Certificates0(trustCertCollectionFile)); + } catch (Exception e) { + throw new IllegalArgumentException("File does not contain valid certificates: " + + trustCertCollectionFile, e); + } + } + + /** + * Trusted certificates for verifying the remote endpoint's certificate. {@code null} uses the system default + * which only works with Java 8u261 and later as these versions support TLS1.3, + * see + * JDK 8u261 Update Release Notes + */ + public QuicSslContextBuilder trustManager(X509Certificate... trustCertCollection) { + try { + return trustManager(QuicheQuicSslContext.buildTrustManagerFactory0(trustCertCollection)); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Trusted manager for verifying the remote endpoint's certificate. {@code null} uses the system default + * which only works with Java 8u261 and later as these versions support TLS1.3, + * see + * JDK 8u261 Update Release Notes + */ + public QuicSslContextBuilder trustManager(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + /** + * A single trusted manager for verifying the remote endpoint's certificate. + * This is helpful when custom implementation of {@link TrustManager} is needed. + * Internally, a simple wrapper of {@link TrustManagerFactory} that only produces this + * specified {@link TrustManager} will be created, thus all the requirements specified in + * {@link #trustManager(TrustManagerFactory trustManagerFactory)} also apply here. + */ + public QuicSslContextBuilder trustManager(TrustManager trustManager) { + return trustManager(new TrustManagerFactoryWrapper(trustManager)); + } + + /** + * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @param keyCertChainFile an X.509 certificate chain file in PEM format + */ + public QuicSslContextBuilder keyManager(File keyFile, String keyPassword, File keyCertChainFile) { + X509Certificate[] keyCertChain; + PrivateKey key; + try { + keyCertChain = QuicheQuicSslContext.toX509Certificates0(keyCertChainFile); + } catch (Exception e) { + throw new IllegalArgumentException("File does not contain valid certificates: " + keyCertChainFile, e); + } + try { + key = QuicheQuicSslContext.toPrivateKey0(keyFile, keyPassword); + } catch (Exception e) { + throw new IllegalArgumentException("File does not contain valid private key: " + keyFile, e); + } + return keyManager(key, keyPassword, keyCertChain); + } + + /** + * Identifying certificate for this host. {@code keyCertChain} and {@code key} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param key a PKCS#8 private key file + * @param keyPassword the password of the {@code key}, or {@code null} if it's not + * password-protected + * @param certChain an X.509 certificate chain + */ + public QuicSslContextBuilder keyManager(PrivateKey key, String keyPassword, X509Certificate... certChain) { + try { + java.security.KeyStore ks = java.security.KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null); + char[] pass = keyPassword == null ? new char[0]: keyPassword.toCharArray(); + ks.setKeyEntry("alias", key, pass, certChain); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(ks, pass); + return keyManager(keyManagerFactory, keyPassword); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Identifying manager for this host. {@code keyManagerFactory} may be {@code null} for + * client contexts, which disables mutual authentication. + */ + public QuicSslContextBuilder keyManager(KeyManagerFactory keyManagerFactory, String keyPassword) { + this.keyPassword = keyPassword; + this.keyManagerFactory = keyManagerFactory; + return this; + } + + /** + * A single key manager managing the identity information of this host. + * This is helpful when custom implementation of {@link KeyManager} is needed. + * Internally, a wrapper of {@link KeyManagerFactory} that only produces this specified + * {@link KeyManager} will be created, thus all the requirements specified in + * {@link #keyManager(KeyManagerFactory, String)} also apply here. + */ + public QuicSslContextBuilder keyManager(KeyManager keyManager, String password) { + return keyManager(new KeyManagerFactoryWrapper(keyManager), password); + } + + /** + * Application protocol negotiation configuration. {@code null} disables support. + */ + public QuicSslContextBuilder applicationProtocols(String... applicationProtocols) { + this.applicationProtocols = applicationProtocols; + return this; + } + + /** + * Set the size of the cache used for storing SSL session objects. {@code 0} to use the + * default value. + */ + public QuicSslContextBuilder sessionCacheSize(long sessionCacheSize) { + this.sessionCacheSize = sessionCacheSize; + return this; + } + + /** + * Set the timeout for the cached SSL session objects, in seconds. {@code 0} to use the + * default value. + */ + public QuicSslContextBuilder sessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + return this; + } + + /** + * Sets the client authentication mode. + */ + public QuicSslContextBuilder clientAuth(ClientAuth clientAuth) { + if (!forServer) { + throw new UnsupportedOperationException("Only supported for server"); + } + this.clientAuth = checkNotNull(clientAuth, "clientAuth"); + return this; + } + + /** + * Create new {@link QuicSslContext} instance with configured settings that can be used for {@code QUIC}. + * + */ + public QuicSslContext build() { + if (forServer) { + return new QuicheQuicSslContext(true, sessionTimeout, sessionCacheSize, clientAuth, trustManagerFactory, + keyManagerFactory, keyPassword, mapping, earlyData, keylog, applicationProtocols); + } else { + return new QuicheQuicSslContext(false, sessionTimeout, sessionCacheSize, clientAuth, trustManagerFactory, + keyManagerFactory, keyPassword, mapping, earlyData, keylog, + applicationProtocols); + } + } + + +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslEngine.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslEngine.java new file mode 100644 index 0000000..c2bd694 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslEngine.java @@ -0,0 +1,23 @@ +/* + * 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.handler.codec.quic; + +import javax.net.ssl.SSLEngine; + +/** + * An {@link SSLEngine} that can be used for QUIC. + */ +public abstract class QuicSslEngine extends SSLEngine { } diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslSessionContext.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslSessionContext.java new file mode 100644 index 0000000..08909b1 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslSessionContext.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import javax.net.ssl.SSLSessionContext; + +/** + * {@link SSLSessionContext} which also supports advanced operations. + */ +public interface QuicSslSessionContext extends SSLSessionContext { + + /** + * Sets the {@link SslSessionTicketKey}s that should be used. The first key of the array is used for encryption + * and decryption while the rest of the array is only used for decryption. This allows you to better handling + * rotating of the keys. The rotating is the responsibility of the user. + * If {@code null} is used for {@code keys} a key will automatically generated by the library and also rotated. + * + * @param keys the tickets to use. + */ + void setTicketKeys(SslSessionTicketKey... keys); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamAddress.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamAddress.java new file mode 100644 index 0000000..529a54a --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamAddress.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import java.net.SocketAddress; +import java.util.Objects; + +/** + * A {@link SocketAddress} for QUIC stream. + */ +public final class QuicStreamAddress extends SocketAddress { + + private final long streamId; + + public QuicStreamAddress(long streamId) { + this.streamId = streamId; + } + + /** + * Return the id of the stream. + * + * @return the id. + */ + public long streamId() { + return streamId; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof QuicStreamAddress)) { + return false; + } + QuicStreamAddress that = (QuicStreamAddress) o; + return streamId == that.streamId; + } + + @Override + public int hashCode() { + return Objects.hash(streamId); + } + + @Override + public String toString() { + return "QuicStreamAddress{" + + "streamId=" + streamId + + '}'; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannel.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannel.java new file mode 100644 index 0000000..dc5d682 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannel.java @@ -0,0 +1,299 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.DuplexChannel; + +import java.net.SocketAddress; + +/** + * A QUIC stream. + */ +public interface QuicStreamChannel extends DuplexChannel { + + /** + * Should be added to a {@link ChannelFuture} when the output should be cleanly shutdown via a {@code FIN}. No more + * writes will be allowed after this point. + */ + ChannelFutureListener SHUTDOWN_OUTPUT = f -> ((QuicStreamChannel) f.channel()).shutdownOutput(); + + @Override + default ChannelFuture bind(SocketAddress socketAddress) { + return pipeline().bind(socketAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress) { + return pipeline().connect(remoteAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { + return pipeline().connect(remoteAddress, localAddress); + } + + @Override + default ChannelFuture disconnect() { + return pipeline().disconnect(); + } + + @Override + default ChannelFuture close() { + return pipeline().close(); + } + + @Override + default ChannelFuture deregister() { + return pipeline().deregister(); + } + + @Override + default ChannelFuture bind(SocketAddress localAddress, ChannelPromise channelPromise) { + return pipeline().bind(localAddress, channelPromise); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise channelPromise) { + return pipeline().connect(remoteAddress, channelPromise); + } + + @Override + default ChannelFuture connect( + SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise channelPromise) { + return pipeline().connect(remoteAddress, localAddress, channelPromise); + } + + @Override + default ChannelFuture disconnect(ChannelPromise channelPromise) { + return pipeline().disconnect(channelPromise); + } + + @Override + default ChannelFuture close(ChannelPromise channelPromise) { + return pipeline().close(channelPromise); + } + + @Override + default ChannelFuture deregister(ChannelPromise channelPromise) { + return pipeline().deregister(channelPromise); + } + + @Override + default ChannelFuture write(Object msg) { + return pipeline().write(msg); + } + + @Override + default ChannelFuture write(Object msg, ChannelPromise channelPromise) { + return pipeline().write(msg, channelPromise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg, ChannelPromise channelPromise) { + return pipeline().writeAndFlush(msg, channelPromise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg) { + return pipeline().writeAndFlush(msg); + } + + @Override + default ChannelPromise newPromise() { + return pipeline().newPromise(); + } + + @Override + default ChannelProgressivePromise newProgressivePromise() { + return pipeline().newProgressivePromise(); + } + + @Override + default ChannelFuture newSucceededFuture() { + return pipeline().newSucceededFuture(); + } + + @Override + default ChannelFuture newFailedFuture(Throwable cause) { + return pipeline().newFailedFuture(cause); + } + + @Override + default ChannelPromise voidPromise() { + return pipeline().voidPromise(); + } + + @Override + default ChannelFuture shutdownInput() { + return shutdownInput(newPromise()); + } + + @Override + default ChannelFuture shutdownInput(ChannelPromise promise) { + return shutdownInput(0, promise); + } + + @Override + default ChannelFuture shutdownOutput() { + return shutdownOutput(newPromise()); + } + + @Override + default ChannelFuture shutdown() { + return shutdown(newPromise()); + } + + /** + * Shortcut for calling {@link #shutdownInput(int)} and {@link #shutdownInput(int)}. + * + * @param error the error to send. + * @return the future that is notified on completion. + */ + default ChannelFuture shutdown(int error) { + return shutdown(error, newPromise()); + } + + /** + * Shortcut for calling {@link #shutdownInput(int, ChannelPromise)} and {@link #shutdownInput(int, ChannelPromise)}. + * + * @param error the error to send. + * @param promise will be notified on completion. + * @return the future that is notified on completion. + */ + ChannelFuture shutdown(int error, ChannelPromise promise); + + /** + * Shutdown the input of the stream with the given error code. This means a {@code STOP_SENDING} frame will + * be send to the remote peer and all data received will be discarded. + * + * @param error the error to send as part of the {@code STOP_SENDING} frame. + * @return the future that is notified on completion. + */ + default ChannelFuture shutdownInput(int error) { + return shutdownInput(error, newPromise()); + } + + /** + * Shutdown the input of the stream with the given error code. This means a {@code STOP_SENDING} frame will + * be send to the remote peer and all data received will be discarded. + * + * @param error the error to send as part of the {@code STOP_SENDING} frame. + * @param promise will be notified on completion. + * @return the future that is notified on completion. + */ + ChannelFuture shutdownInput(int error, ChannelPromise promise); + + /** + * Shutdown the output of the stream with the given error code. This means a {@code RESET_STREAM} frame will + * be send to the remote peer and all data that is not sent yet will be discarded. + * + * Important:If you want to shutdown the output without sending a {@code RESET_STREAM} frame you + * should use {@link #shutdownOutput()} which will shutdown the output by sending a {@code FIN} and so signal + * a clean shutdown. + * + * @param error the error to send as part of the {@code RESET_STREAM} frame. + * @return the future that is notified on completion. + */ + default ChannelFuture shutdownOutput(int error) { + return shutdownOutput(error, newPromise()); + } + + /** + * Shutdown the output of the stream with the given error code. This means a {@code RESET_STREAM} frame will + * be send to the remote peer and all data that is not sent yet will be discarded. + * + * Important:If you want to shutdown the output without sending a {@code RESET_STREAM} frame you + * should use {@link #shutdownOutput(ChannelPromise)} which will shutdown the output by sending a {@code FIN} + * and so signal a clean shutdown. + * + * @param error the error to send as part of the {@code RESET_STREAM} frame. + * @param promise will be notified on completion. + * @return the future that is notified on completion. + */ + ChannelFuture shutdownOutput(int error, ChannelPromise promise); + + @Override + QuicStreamAddress localAddress(); + + @Override + QuicStreamAddress remoteAddress(); + + /** + * Returns {@code true} if the stream was created locally. + * + * @return {@code true} if created locally, {@code false} otherwise. + */ + boolean isLocalCreated(); + + /** + * Returns the {@link QuicStreamType} of the stream. + * + * @return {@link QuicStreamType} of this stream. + */ + QuicStreamType type(); + + /** + * The id of the stream. + * + * @return the stream id of this {@link QuicStreamChannel}. + */ + long streamId(); + + /** + * The {@link QuicStreamPriority} if explicit set for the stream via {@link #updatePriority(QuicStreamPriority)} or + * {@link #updatePriority(QuicStreamPriority, ChannelPromise)}. Otherwise {@code null}. + * + * @return the priority if any was set. + */ + QuicStreamPriority priority(); + + /** + * Update the priority of the stream. A stream's priority determines the order in which stream data is sent + * on the wire (streams with lower priority are sent first). + * + * @param priority the priority. + * @return future that is notified once the operation completes. + */ + default ChannelFuture updatePriority(QuicStreamPriority priority) { + return updatePriority(priority, newPromise()); + } + + /** + * Update the priority of the stream. A stream's priority determines the order in which stream data is sent + * on the wire (streams with lower priority are sent first). + * + * @param priority the priority. + * @param promise notified once operations completes. + * @return future that is notified once the operation completes. + */ + ChannelFuture updatePriority(QuicStreamPriority priority, ChannelPromise promise); + + @Override + QuicChannel parent(); + + @Override + QuicStreamChannel read(); + + @Override + QuicStreamChannel flush(); + + @Override + QuicStreamChannelConfig config(); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelBootstrap.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelBootstrap.java new file mode 100644 index 0000000..cc59c00 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelBootstrap.java @@ -0,0 +1,148 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Allows to bootstrap outgoing {@link QuicStreamChannel}s. + */ +public final class QuicStreamChannelBootstrap { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicStreamChannelBootstrap.class); + + private final QuicChannel parent; + private final Map, Object> options = new LinkedHashMap<>(); + private final Map, Object> attrs = new HashMap<>(); + private ChannelHandler handler; + private QuicStreamType type = QuicStreamType.BIDIRECTIONAL; + + /** + * Creates a new instance which uses the given {@link QuicChannel} to bootstrap {@link QuicStreamChannel}s. + * + * @param parent the {@link QuicChannel} that is used. + + */ + QuicStreamChannelBootstrap(QuicChannel parent) { + this.parent = ObjectUtil.checkNotNull(parent, "parent"); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicStreamChannel}. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicStreamChannelBootstrap option(ChannelOption option, T value) { + Quic.updateOptions(options, option, value); + return this; + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicChannel}. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicStreamChannelBootstrap attr(AttributeKey key, T value) { + Quic.updateAttributes(attrs, key, value); + return this; + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} once created. + * + * @param streamHandler the {@link ChannelHandler} that is added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicStreamChannelBootstrap handler(ChannelHandler streamHandler) { + this.handler = ObjectUtil.checkNotNull(streamHandler, "streamHandler"); + return this; + } + + /** + * Set the {@link QuicStreamType} to use for the {@link QuicStreamChannel}, default is + * {@link QuicStreamType#BIDIRECTIONAL}. + * + * @param type the {@link QuicStreamType} of the {@link QuicStreamChannel}. + * @return this instance. + */ + public QuicStreamChannelBootstrap type(QuicStreamType type) { + this.type = ObjectUtil.checkNotNull(type, "type"); + return this; + } + + /** + * Creates a new {@link QuicStreamChannel} and notifies the {@link Future}. + * + * @return the {@link Future} that is notified once the operation completes. + */ + public Future create() { + return create(parent.eventLoop().newPromise()); + } + + /** + * Creates a new {@link QuicStreamChannel} and notifies the {@link Future}. + * + * @param promise the {@link Promise} that is notified once the operation completes. + * @return the {@link Future} that is notified once the operation completes. + */ + public Future create(Promise promise) { + if (handler == null) { + throw new IllegalStateException("streamHandler not set"); + } + + return parent.createStream(type, new QuicStreamChannelBootstrapHandler(handler, + Quic.toOptionsArray(options), Quic.toAttributesArray(attrs)), promise); + } + + private static final class QuicStreamChannelBootstrapHandler extends ChannelInitializer { + private final ChannelHandler streamHandler; + private final Map.Entry, Object>[] streamOptions; + private final Map.Entry, Object>[] streamAttrs; + + QuicStreamChannelBootstrapHandler(ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptions, + Map.Entry, Object>[] streamAttrs) { + this.streamHandler = streamHandler; + this.streamOptions = streamOptions; + this.streamAttrs = streamAttrs; + } + @Override + protected void initChannel(QuicStreamChannel ch) { + Quic.setupChannel(ch, streamOptions, streamAttrs, streamHandler, logger); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelConfig.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelConfig.java new file mode 100644 index 0000000..951fa4a --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelConfig.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.DuplexChannelConfig; + +/** + * {@link DuplexChannelConfig} for QUIC streams. + */ +public interface QuicStreamChannelConfig extends DuplexChannelConfig { + /** + * Set this to {@code true} if the {@link QuicStreamChannel} should read {@link QuicStreamFrame}s and fire these + * through the {@link io.netty.channel.ChannelPipeline}, {@code false} if it uses {@link io.netty.buffer.ByteBuf}. + * + * @param readFrames {@code true} if {@link QuicStreamFrame}s should be used, {@code false} if + * {@link io.netty.buffer.ByteBuf} should be used. + * @return this instance itself. + * + */ + QuicStreamChannelConfig setReadFrames(boolean readFrames); + + /** + * Returns {@code true} if the {@link QuicStreamChannel} will read {@link QuicStreamFrame}s and fire these through + * the {@link io.netty.channel.ChannelPipeline}, {@code false} if it uses {@link io.netty.buffer.ByteBuf}. + * + * @return {@code true} if {@link QuicStreamFrame}s should be used, {@code false} if + * {@link io.netty.buffer.ByteBuf} should be used. + */ + boolean isReadFrames(); + + @Override + QuicStreamChannelConfig setAllowHalfClosure(boolean allowHalfClosure); + + @Override + QuicStreamChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead); + + @Override + QuicStreamChannelConfig setWriteSpinCount(int writeSpinCount); + + @Override + QuicStreamChannelConfig setAllocator(ByteBufAllocator allocator); + + @Override + QuicStreamChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator); + + @Override + QuicStreamChannelConfig setAutoRead(boolean autoRead); + + @Override + QuicStreamChannelConfig setAutoClose(boolean autoClose); + + @Override + QuicStreamChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator); + + @Override + QuicStreamChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark); + + @Override + QuicStreamChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis); + + @Override + QuicStreamChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark); + + @Override + QuicStreamChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamFrame.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamFrame.java new file mode 100644 index 0000000..1fcdd2c --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamFrame.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.Unpooled; + +/** + * A QUIC STREAM_FRAME. + */ +public interface QuicStreamFrame extends ByteBufHolder { + + /** + * An empty {@link QuicStreamFrame} that has the {@code FIN} flag set. + */ + QuicStreamFrame EMPTY_FIN = new QuicStreamFrame() { + @Override + public boolean hasFin() { + return true; + } + + @Override + public QuicStreamFrame copy() { + return this; + } + + @Override + public QuicStreamFrame duplicate() { + return this; + } + + @Override + public QuicStreamFrame retainedDuplicate() { + return this; + } + + @Override + public QuicStreamFrame replace(ByteBuf content) { + return new DefaultQuicStreamFrame(content, hasFin()); + } + + @Override + public QuicStreamFrame retain() { + return this; + } + + @Override + public QuicStreamFrame retain(int increment) { + return this; + } + + @Override + public QuicStreamFrame touch() { + return this; + } + + @Override + public QuicStreamFrame touch(Object hint) { + return this; + } + + @Override + public ByteBuf content() { + return Unpooled.EMPTY_BUFFER; + } + + @Override + public int refCnt() { + return 1; + } + + @Override + public boolean release() { + return false; + } + + @Override + public boolean release(int decrement) { + return false; + } + }; + + /** + * Returns {@code true} if the frame has the FIN set, which means it notifies the remote peer that + * there will be no more writing happen. {@code false} otherwise. + * + * @return {@code true} if the FIN flag should be set, {@code false} otherwise. + */ + boolean hasFin(); + + @Override + QuicStreamFrame copy(); + + @Override + QuicStreamFrame duplicate(); + + @Override + QuicStreamFrame retainedDuplicate(); + + @Override + QuicStreamFrame replace(ByteBuf content); + + @Override + QuicStreamFrame retain(); + + @Override + QuicStreamFrame retain(int increment); + + @Override + QuicStreamFrame touch(); + + @Override + QuicStreamFrame touch(Object hint); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamIdGenerator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamIdGenerator.java new file mode 100644 index 0000000..7e3e3c3 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamIdGenerator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +/** + * Generates and hands over the next stream id to use for a QUIC stream. + */ +final class QuicStreamIdGenerator { + private long nextBidirectionalStreamId; + private long nextUnidirectionalStreamId; + + QuicStreamIdGenerator(boolean server) { + // See https://quicwg.org/base-drafts/rfc9000.html#name-stream-types-and-identifier + nextBidirectionalStreamId = server ? 1 : 0; + nextUnidirectionalStreamId = server ? 3 : 2; + } + + long nextStreamId(boolean bidirectional) { + if (bidirectional) { + long stream = nextBidirectionalStreamId; + nextBidirectionalStreamId += 4; + return stream; + } + long stream = nextUnidirectionalStreamId; + nextUnidirectionalStreamId += 4; + return stream; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamLimitChangedEvent.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamLimitChangedEvent.java new file mode 100644 index 0000000..8cd4063 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamLimitChangedEvent.java @@ -0,0 +1,26 @@ +/* + * 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.handler.codec.quic; + +/** + * Event fired once the stream limit of a {@link QuicChannel} changes. + */ +public final class QuicStreamLimitChangedEvent implements QuicEvent { + + static final QuicStreamLimitChangedEvent INSTANCE = new QuicStreamLimitChangedEvent(); + + private QuicStreamLimitChangedEvent() { } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamPriority.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamPriority.java new file mode 100644 index 0000000..570ca78 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamPriority.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.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +import java.util.Objects; + +/** + * The priority of a {@link QuicStreamChannel}. + */ +public final class QuicStreamPriority { + + private final int urgency; + private final boolean incremental; + + /** + * Create a new instance + * + * @param urgency the urgency of the stream. + * @param incremental {@code true} if incremental. + */ + public QuicStreamPriority(int urgency, boolean incremental) { + this.urgency = ObjectUtil.checkInRange(urgency, 0, Byte.MAX_VALUE, "urgency"); + this.incremental = incremental; + } + + /** + * The urgency of the stream. Smaller number means more urgent and so data will be send earlier. + * + * @return the urgency. + */ + public int urgency() { + return urgency; + } + + /** + * {@code true} if incremental, {@code false} otherwise. + * + * @return if incremental. + */ + public boolean isIncremental() { + return incremental; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QuicStreamPriority that = (QuicStreamPriority) o; + return urgency == that.urgency && incremental == that.incremental; + } + + @Override + public int hashCode() { + return Objects.hash(urgency, incremental); + } + + @Override + public String toString() { + return "QuicStreamPriority{" + + "urgency=" + urgency + + ", incremental=" + incremental + + '}'; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamType.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamType.java new file mode 100644 index 0000000..268c5d8 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamType.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +/** + * The type of a {@link QuicStreamChannel}. + */ +public enum QuicStreamType { + + /** + * An unidirectional stream. + */ + UNIDIRECTIONAL, + /** + * A bidirectional stream. + */ + BIDIRECTIONAL +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicTokenHandler.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicTokenHandler.java new file mode 100644 index 0000000..729d83b --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicTokenHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; + +import java.net.InetSocketAddress; + +/** + * Handle token related operations. + */ +public interface QuicTokenHandler { + + /** + * Generate a new token for the given destination connection id and address. This token is written to {@code out}. + * If no token should be generated and so no token validation should take place at all this method should return + * {@code false}. + * + * @param out {@link ByteBuf} into which the token will be written. + * @param dcid the destination connection id. + * @param address the {@link InetSocketAddress} of the sender. + * @return {@code true} if a token was written and so validation should happen, {@code false} otherwise. + */ + boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address); + + /** + * Validate the token and return the offset, {@code -1} is returned if the token is not valid. + * + * @param token the {@link ByteBuf} that contains the token. The ownership is not transferred. + * @param address the {@link InetSocketAddress} of the sender. + * @return the start index after the token or {@code -1} if the token was not valid. + */ + int validateToken(ByteBuf token, InetSocketAddress address); + + /** + * Return the maximal token length. + * + * @return the maximal supported token length. + */ + int maxTokenLength(); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicTransportParameters.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicTransportParameters.java new file mode 100644 index 0000000..9bf1cc2 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicTransportParameters.java @@ -0,0 +1,113 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +/** + * Transport parameters for QUIC. + */ +public interface QuicTransportParameters { + + /** + * The maximum idle timeout. + * @return timeout. + */ + long maxIdleTimeout(); + + /** + * The maximum UDP payload size. + * + * @return maximum payload size. + */ + long maxUdpPayloadSize(); + + /** + * The initial flow control maximum data for the connection. + * + * @return flowcontrol. + */ + long initialMaxData(); + + /** + * The initial flow control maximum data for local bidirectional streams. + * + * @return flowcontrol. + */ + long initialMaxStreamDataBidiLocal(); + + /** + * The initial flow control maximum data for remote bidirectional streams. + * + * @return flowcontrol. + */ + long initialMaxStreamDataBidiRemote(); + + /** + * The initial flow control maximum data for unidirectional streams. + * + * @return flowcontrol. + */ + long initialMaxStreamDataUni(); + + + /** + * The initial maximum bidirectional streams. + * + * @return streams. + */ + long initialMaxStreamsBidi(); + + /** + * The initial maximum unidirectional streams. + * + * @return streams. + */ + long initialMaxStreamsUni(); + + /** + * The ACK delay exponent + * + * @return exponent. + */ + long ackDelayExponent(); + + /** + * The max ACK delay. + * + * @return delay. + */ + long maxAckDelay(); + + /** + * Whether active migration is disabled. + * + * @return disabled. + */ + boolean disableActiveMigration(); + + /** + * The active connection ID limit. + * + * @return limit. + */ + long activeConnIdLimit(); + + /** + * DATAGRAM frame extension parameter, if any. + * + * @return param. + */ + long maxDatagramFrameSize(); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Quiche.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Quiche.java new file mode 100644 index 0000000..e8a1ce7 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/Quiche.java @@ -0,0 +1,870 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ClassInitializerUtil; +import io.netty.util.internal.NativeLibraryLoader; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +final class Quiche { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(Quiche.class); + private static final boolean DEBUG_LOGGING_ENABLED = logger.isDebugEnabled(); + + static { + // Preload all classes that will be used in the OnLoad(...) function of JNI to eliminate the possiblity of a + // class-loader deadlock. This is a workaround for https://github.com/netty/netty/issues/11209. + + // This needs to match all the classes that are loaded via NETTY_JNI_UTIL_LOAD_CLASS or looked up via + // NETTY_JNI_UTIL_FIND_CLASS. + ClassInitializerUtil.tryLoadClasses(Quiche.class, + // netty_quic_boringssl + byte[].class, String.class, BoringSSLCertificateCallback.class, + BoringSSLCertificateVerifyCallback.class, BoringSSLHandshakeCompleteCallback.class, + + //netty_quic_quiche + QuicheLogger.class + ); + + try { + // First, try calling a side-effect free JNI method to see if the library was already + // loaded by the application. + quiche_version(); + } catch (UnsatisfiedLinkError ignore) { + // The library was not previously loaded, load it now. + loadNativeLibrary(); + } + + // Let's enable debug logging for quiche if its enabled in our logger. + if (DEBUG_LOGGING_ENABLED) { + quiche_enable_debug_logging(new QuicheLogger(logger)); + } + } + + private static void loadNativeLibrary() { + // This needs to be kept in sync with what is defined in netty_quic_quiche.c + String libName = "netty_quiche"; + ClassLoader cl = PlatformDependent.getClassLoader(Quiche.class); + + if (!PlatformDependent.isAndroid()) { + libName += '_' + PlatformDependent.normalizedOs() + + '_' + PlatformDependent.normalizedArch(); + } + + try { + NativeLibraryLoader.load(libName, cl); + } catch (UnsatisfiedLinkError e) { + logger.debug("Failed to load {}", libName, e); + throw e; + } + } + + static final short AF_INET = (short) QuicheNativeStaticallyReferencedJniMethods.afInet(); + static final short AF_INET6 = (short) QuicheNativeStaticallyReferencedJniMethods.afInet6(); + static final int SIZEOF_SOCKADDR_STORAGE = QuicheNativeStaticallyReferencedJniMethods.sizeofSockaddrStorage(); + static final int SIZEOF_SOCKADDR_IN = QuicheNativeStaticallyReferencedJniMethods.sizeofSockaddrIn(); + static final int SIZEOF_SOCKADDR_IN6 = QuicheNativeStaticallyReferencedJniMethods.sizeofSockaddrIn6(); + static final int SOCKADDR_IN_OFFSETOF_SIN_FAMILY = + QuicheNativeStaticallyReferencedJniMethods.sockaddrInOffsetofSinFamily(); + static final int SOCKADDR_IN_OFFSETOF_SIN_PORT = + QuicheNativeStaticallyReferencedJniMethods.sockaddrInOffsetofSinPort(); + static final int SOCKADDR_IN_OFFSETOF_SIN_ADDR = + QuicheNativeStaticallyReferencedJniMethods.sockaddrInOffsetofSinAddr(); + static final int IN_ADDRESS_OFFSETOF_S_ADDR = QuicheNativeStaticallyReferencedJniMethods.inAddressOffsetofSAddr(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_FAMILY = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Family(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_PORT = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Port(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_FLOWINFO = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Flowinfo(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_ADDR = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Addr(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_SCOPE_ID = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6ScopeId(); + static final int IN6_ADDRESS_OFFSETOF_S6_ADDR = + QuicheNativeStaticallyReferencedJniMethods.in6AddressOffsetofS6Addr(); + static final int SIZEOF_SOCKLEN_T = QuicheNativeStaticallyReferencedJniMethods.sizeofSocklenT(); + static final int SIZEOF_SIZE_T = QuicheNativeStaticallyReferencedJniMethods.sizeofSizeT(); + + static final int SIZEOF_TIMESPEC = QuicheNativeStaticallyReferencedJniMethods.sizeofTimespec(); + + static final int SIZEOF_TIME_T = QuicheNativeStaticallyReferencedJniMethods.sizeofTimeT(); + static final int SIZEOF_LONG = QuicheNativeStaticallyReferencedJniMethods.sizeofLong(); + + static final int TIMESPEC_OFFSETOF_TV_SEC = + QuicheNativeStaticallyReferencedJniMethods.timespecOffsetofTvSec(); + + static final int TIMESPEC_OFFSETOF_TV_NSEC = + QuicheNativeStaticallyReferencedJniMethods.timespecOffsetofTvNsec(); + + static final int QUICHE_RECV_INFO_OFFSETOF_FROM = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofFrom(); + static final int QUICHE_RECV_INFO_OFFSETOF_FROM_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofFromLen(); + + static final int QUICHE_RECV_INFO_OFFSETOF_TO = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofTo(); + static final int QUICHE_RECV_INFO_OFFSETOF_TO_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofToLen(); + + static final int SIZEOF_QUICHE_RECV_INFO = QuicheNativeStaticallyReferencedJniMethods.sizeofQuicheRecvInfo(); + static final int QUICHE_SEND_INFO_OFFSETOF_TO = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofTo(); + static final int QUICHE_SEND_INFO_OFFSETOF_TO_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofToLen(); + + static final int QUICHE_SEND_INFO_OFFSETOF_FROM = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofFrom(); + static final int QUICHE_SEND_INFO_OFFSETOF_FROM_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofFromLen(); + + static final int QUICHE_SEND_INFO_OFFSETOF_AT = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofAt(); + static final int SIZEOF_QUICHE_SEND_INFO = QuicheNativeStaticallyReferencedJniMethods.sizeofQuicheSendInfo(); + + static final int QUICHE_PROTOCOL_VERSION = QuicheNativeStaticallyReferencedJniMethods.quiche_protocol_version(); + static final int QUICHE_MAX_CONN_ID_LEN = QuicheNativeStaticallyReferencedJniMethods.quiche_max_conn_id_len(); + + /** + * See QUICHE_SHUTDOWN_READ. + */ + static final int QUICHE_SHUTDOWN_READ = QuicheNativeStaticallyReferencedJniMethods.quiche_shutdown_read(); + + /** + * See QUICHE_SHUTDOWN_WRITE. + */ + static final int QUICHE_SHUTDOWN_WRITE = QuicheNativeStaticallyReferencedJniMethods.quiche_shutdown_write(); + + /** + * See QUICHE_ERR_DONE. + */ + static final int QUICHE_ERR_DONE = QuicheNativeStaticallyReferencedJniMethods.quiche_err_done(); + + /** + * See + * QUICHE_ERR_BUFFER_TOO_SHORT. + */ + static final int QUICHE_ERR_BUFFER_TOO_SHORT = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_buffer_too_short(); + + /** + * See + * QUICHE_ERR_UNKNOWN_VERSION. + */ + static final int QUICHE_ERR_UNKNOWN_VERSION = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_unknown_version(); + + /** + * See + * QUICHE_ERR_INVALID_FRAME. + */ + static final int QUICHE_ERR_INVALID_FRAME = QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_frame(); + + /** + * See + * QUICHE_ERR_INVALID_PACKET. + */ + static final int QUICHE_ERR_INVALID_PACKET = QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_packet(); + + /** + * See + * QUICHE_ERR_INVALID_STATE. + */ + static final int QUICHE_ERR_INVALID_STATE = QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_state(); + + /** + * See + * QUICHE_ERR_INVALID_STREAM_STATE. + */ + static final int QUICHE_ERR_INVALID_STREAM_STATE = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_stream_state(); + + /** + * See + * QUICHE_ERR_INVALID_TRANSPORT_PARAM. + */ + static final int QUICHE_ERR_INVALID_TRANSPORT_PARAM = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_transport_param(); + + /** + * See + * QUICHE_ERR_CRYPTO_FAIL. + */ + static final int QUICHE_ERR_CRYPTO_FAIL = QuicheNativeStaticallyReferencedJniMethods.quiche_err_crypto_fail(); + + /** + * See + * QUICHE_ERR_TLS_FAIL. + */ + static final int QUICHE_ERR_TLS_FAIL = QuicheNativeStaticallyReferencedJniMethods.quiche_err_tls_fail(); + + /** + * See + * QUICHE_ERR_FLOW_CONTROL. + */ + static final int QUICHE_ERR_FLOW_CONTROL = QuicheNativeStaticallyReferencedJniMethods.quiche_err_flow_control(); + + /** + * See + * QUICHE_ERR_STREAM_LIMIT. + */ + static final int QUICHE_ERR_STREAM_LIMIT = QuicheNativeStaticallyReferencedJniMethods.quiche_err_stream_limit(); + + /** + * See + * QUICHE_ERR_FINAL_SIZE. + */ + static final int QUICHE_ERR_FINAL_SIZE = QuicheNativeStaticallyReferencedJniMethods.quiche_err_final_size(); + + /** + * See + * QUICHE_ERR_CONGESTION_CONTROL. + */ + static final int QUICHE_ERR_CONGESTION_CONTROL = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_congestion_control(); + + /** + * See QUICHE_ERR_STREAM_STOPPED. + */ + static final int QUICHE_ERR_STREAM_RESET = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_stream_reset(); + + /** + * See + * QUICHE_ERR_STREAM_STOPPED. + */ + static final int QUICHE_ERR_STREAM_STOPPED = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_stream_stopped(); + + + // Too many identifiers were provided. + static final int QUICHE_ERR_ID_LIMIT = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_id_limit(); + + // Not enough available identifiers. + static final int QUICHE_ERR_OUT_OF_IDENTIFIERS = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_out_of_identifiers(); + + // Error in key update. + static final int QUICHE_ERR_KEY_UPDATE = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_key_update(); + + /** + * See + * QUICHE_CC_RENO. + */ + static final int QUICHE_CC_RENO = QuicheNativeStaticallyReferencedJniMethods.quiche_cc_reno(); + + /** + * See + * QUICHE_CC_CUBIC. + */ + static final int QUICHE_CC_CUBIC = QuicheNativeStaticallyReferencedJniMethods.quiche_cc_cubic(); + + /** + * See + * QUICHE_CC_BBR. + */ + static final int QUICHE_CC_BBR = QuicheNativeStaticallyReferencedJniMethods.quiche_cc_bbr(); + + + static final int QUICHE_PATH_EVENT_NEW = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_new(); + static final int QUICHE_PATH_EVENT_VALIDATED = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_validated(); + static final int QUICHE_PATH_EVENT_FAILED_VALIDATION = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_failed_validation(); + static final int QUICHE_PATH_EVENT_CLOSED = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_closed(); + static final int QUICHE_PATH_EVENT_REUSED_SOURCE_CONNECTION_ID = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_reused_source_connection_id(); + static final int QUICHE_PATH_EVENT_PEER_MIGRATED = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_peer_migrated(); + + /** + * See quiche_version. + */ + static native String quiche_version(); + + /** + * See + * quiche_version_is_supported. + */ + static native boolean quiche_version_is_supported(int version); + + /** + * See quiche_header_info. + */ + static native int quiche_header_info(long bufAddr, int bufLength, int dcil, long versionAddr, long typeAddr, + long scidAddr, long scidLenAddr, long dcidAddr, long dcidLenAddr, + long tokenAddr, long tokenLenAddr); + /** + * See quiche_negotiate_version. + */ + static native int quiche_negotiate_version( + long scidAddr, int scidLen, long dcidAddr, int dcidLen, long outAddr, int outLen); + + /** + * See quiche_retry. + */ + static native int quiche_retry(long scidAddr, int scidLen, long dcidAddr, int dcidLen, long newScidAddr, + int newScidLen, long tokenAddr, int tokenLen, int version, long outAddr, int outLen); + + /** + * See quiche_conn_new_with_tls. + */ + static native long quiche_conn_new_with_tls(long scidAddr, int scidLen, long odcidAddr, int odcidLen, + long localAddr, int localLen, + long peerAddr, int peerLen, + long configAddr, long ssl, boolean isServer); + + /** + * See + * quiche_conn_set_qlog_path. + */ + static native boolean quiche_conn_set_qlog_path(long connAddr, String path, String logTitle, String logDescription); + + /** + * See quiche_conn_recv. + */ + static native int quiche_conn_recv(long connAddr, long bufAddr, int bufLen, long infoAddr); + + /** + * See quiche_conn_send. + */ + static native int quiche_conn_send(long connAddr, long outAddr, int outLen, long infoAddr); + + /** + * See quiche_conn_free. + */ + static native void quiche_conn_free(long connAddr); + + static QuicConnectionCloseEvent quiche_conn_peer_error(long connAddr) { + Object[] error = quiche_conn_peer_error0(connAddr); + if (error == null) { + return null; + } + return new QuicConnectionCloseEvent((Boolean) error[0], (Integer) error[1], (byte[]) error[2]); + } + + private static native Object[] quiche_conn_peer_error0(long connAddr); + + /** + * See + * quiche_conn_peer_streams_left_bidi. + */ + static native long quiche_conn_peer_streams_left_bidi(long connAddr); + + /** + * See + * quiche_conn_peer_streams_left_uni. + */ + static native long quiche_conn_peer_streams_left_uni(long connAddr); + + /** + * See + * quiche_conn_stream_priority. + */ + static native int quiche_conn_stream_priority( + long connAddr, long streamId, byte urgency, boolean incremental); + + static native int quiche_conn_send_quantum(long connAddr); + + /** + * See quiche_conn_trace_id. + */ + static native byte[] quiche_conn_trace_id(long connAddr); + + static native byte[] quiche_conn_source_id(long connAddr); + + static native byte[] quiche_conn_destination_id(long connAddr); + + /** + * See quiche_conn_stream_recv. + */ + static native int quiche_conn_stream_recv(long connAddr, long streamId, long outAddr, int bufLen, long finAddr); + + /** + * See quiche_conn_stream_send. + */ + static native int quiche_conn_stream_send(long connAddr, long streamId, long bufAddr, int bufLen, boolean fin); + + /** + * See + * quiche_conn_stream_shutdown. + */ + static native int quiche_conn_stream_shutdown(long connAddr, long streamId, int direction, long err); + + /** + * See + * quiche_conn_stream_capacity. + */ + static native int quiche_conn_stream_capacity(long connAddr, long streamId); + + /** + * See + * quiche_conn_stream_finished. + */ + static native boolean quiche_conn_stream_finished(long connAddr, long streamId); + + /** + * See + * quiche_conn_close. + */ + static native int quiche_conn_close(long connAddr, boolean app, long err, long reasonAddr, int reasonLen); + + /** + * See + * quiche_conn_is_established. + */ + static native boolean quiche_conn_is_established(long connAddr); + + /** + * See + * quiche_conn_is_in_early_data. + */ + static native boolean quiche_conn_is_in_early_data(long connAddr); + + /** + * See + * quiche_conn_is_closed. + */ + static native boolean quiche_conn_is_closed(long connAddr); + + /** + * See + * quiche_conn_is_timed_out. + */ + static native boolean quiche_conn_is_timed_out(long connAddr); + + /** + * See + * quiche_conn_stats. + * The implementation relies on all fields of + * quiche_stats being numerical. + * The assumption made allows passing primitive array rather than dealing with objects. + */ + static native long[] quiche_conn_stats(long connAddr); + + /** + * See + * + * quiche_conn_stats. + */ + static native long[] quiche_conn_peer_transport_params(long connAddr); + + /** + * See + * quiche_conn_timeout_as_nanos. + */ + static native long quiche_conn_timeout_as_nanos(long connAddr); + + /** + * See + * quiche_conn_on_timeout. + */ + static native void quiche_conn_on_timeout(long connAddr); + + /** + * See + * quiche_conn_readable. + */ + static native long quiche_conn_readable(long connAddr); + + /** + * See + * quiche_conn_writable. + */ + static native long quiche_conn_writable(long connAddr); + + /** + * See + * quiche_stream_iter_next. + * + * This method will fill the {@code streamIds} array and return the number of streams that were filled into + * the array. If the number is the same as the length of the array you should call it again until it returns + * less to ensure you process all the streams later on. + */ + static native int quiche_stream_iter_next(long iterAddr, long[] streamIds); + + /** + * See + * quiche_stream_iter_free. + * + */ + static native void quiche_stream_iter_free(long iterAddr); + + /** + * See + * + * quiche_conn_dgram_max_writable_len. + */ + static native int quiche_conn_dgram_max_writable_len(long connAddr); + + /** + * See + * + * quiche_conn_dgram_recv_front_len. + */ + static native int quiche_conn_dgram_recv_front_len(long connAddr); + + /** + * See + * + * quiche_conn_dgram_recv. + */ + static native int quiche_conn_dgram_recv(long connAddr, long buf, int size); + + /** + * See + * + * quiche_conn_dgram_send. + */ + 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_conn_max_send_udp_payload_size. + */ + static native int quiche_conn_max_send_udp_payload_size(long connAddr); + + static native int quiche_conn_scids_left(long connAddr); + + static native long quiche_conn_new_scid(long connAddr, long scidAddr, int scidLen, byte[] resetToken, boolean retire_if_needed, long seq); + + static native byte[] quiche_conn_retired_scid_next(long connAddr); + + static native long quiche_conn_path_event_next(long connAddr); + static native int quiche_path_event_type(long pathEvent); + static native void quiche_path_event_free(long pathEvent); + static native Object[] quiche_path_event_new(long pathEvent); + static native Object[] quiche_path_event_validated(long pathEvent); + static native Object[] quiche_path_event_failed_validation(long pathEvent); + static native Object[] quiche_path_event_closed(long pathEvent); + static native Object[] quiche_path_event_reused_source_connection_id(long pathEvent); + static native Object[] quiche_path_event_peer_migrated(long pathEvent); + + /** + * See + * quiche_config_new. + */ + static native long quiche_config_new(int version); + + /** + * See + * + * quiche_config_grease. + */ + static native void quiche_config_grease(long configAddr, boolean value); + + /** + * See + * + * quiche_config_set_max_idle_timeout. + */ + static native void quiche_config_set_max_idle_timeout(long configAddr, long value); + + /** + * See + * + * quiche_config_set_max_recv_udp_payload_size. + */ + static native void quiche_config_set_max_recv_udp_payload_size(long configAddr, long value); + + /** + * See + * + * quiche_config_set_max_recv_udp_payload_size. + */ + static native void quiche_config_set_max_send_udp_payload_size(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_data. + */ + static native void quiche_config_set_initial_max_data(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_stream_data_bidi_local. + */ + static native void quiche_config_set_initial_max_stream_data_bidi_local(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_stream_data_bidi_remote. + */ + static native void quiche_config_set_initial_max_stream_data_bidi_remote(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_stream_data_uni. + */ + static native void quiche_config_set_initial_max_stream_data_uni(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_streams_bidi. + */ + static native void quiche_config_set_initial_max_streams_bidi(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_streams_uni. + */ + static native void quiche_config_set_initial_max_streams_uni(long configAddr, long value); + + /** + * See + * + * quiche_config_set_ack_delay_exponent. + */ + static native void quiche_config_set_ack_delay_exponent(long configAddr, long value); + + /** + * See + * + * quiche_config_set_max_ack_delay. + */ + static native void quiche_config_set_max_ack_delay(long configAddr, long value); + + /** + * See + * + * quiche_config_set_disable_active_migration. + */ + static native void quiche_config_set_disable_active_migration(long configAddr, boolean value); + + /** + * See + * + * quiche_config_set_cc_algorithm. + */ + static native void quiche_config_set_cc_algorithm(long configAddr, int algo); + + /** + * See + * + * quiche_config_enable_hystart. + */ + static native void quiche_config_enable_hystart(long configAddr, boolean value); + + /** + * See + * + * quiche_config_enable_dgram. + */ + static native void quiche_config_enable_dgram(long configAddr, boolean enable, + int recv_queue_len, int send_queue_len); + + // Sets the limit of active connection IDs. + static native void quiche_config_set_active_connection_id_limit(long configAddr, long value); + + // Sets the initial stateless reset token. + static native void quiche_config_set_stateless_reset_token(long configAddr, byte[] token); + + /** + * See + * + * quiche_config_free. + */ + static native void quiche_config_free(long configAddr); + + /** + * See + * quiche_config_new. + */ + private static native void quiche_enable_debug_logging(QuicheLogger logger); + + private static native long buffer_memory_address(ByteBuffer buffer); + + static native int sockaddr_cmp(long addr, long addr2); + + /** + * Returns the memory address if the {@link ByteBuf} taking the readerIndex into account. + * + * @param buf the {@link ByteBuf} of which we want to obtain the memory address + * (taking its {@link ByteBuf#readerIndex()} into account). + * @return the memory address of this {@link ByteBuf}s readerIndex. + */ + static long readerMemoryAddress(ByteBuf buf) { + return memoryAddress(buf, buf.readerIndex(), buf.readableBytes()); + } + + /** + * Returns the memory address if the {@link ByteBuf} taking the writerIndex into account. + * + * @param buf the {@link ByteBuf} of which we want to obtain the memory address + * (taking its {@link ByteBuf#writerIndex()} into account). + * @return the memory address of this {@link ByteBuf}s writerIndex. + */ + static long writerMemoryAddress(ByteBuf buf) { + return memoryAddress(buf, buf.writerIndex(), buf.writableBytes()); + } + + /** + * Returns the memory address if the {@link ByteBuf} taking the offset into account. + * + * @param buf the {@link ByteBuf} of which we want to obtain the memory address + * (taking the {@code offset} into account). + * @param offset the offset of the memory address. + * @param len the length of the {@link ByteBuf}. + * @return the memory address of this {@link ByteBuf}s offset. + */ + static long memoryAddress(ByteBuf buf, int offset, int len) { + assert buf.isDirect(); + if (buf.hasMemoryAddress()) { + return buf.memoryAddress() + offset; + } + return memoryAddressWithPosition(buf.internalNioBuffer(offset, len)); + } + + /** + * Returns the memory address of the given {@link ByteBuffer} taking its current {@link ByteBuffer#position()} into + * account. + * + * @param buf the {@link ByteBuffer} of which we want to obtain the memory address + * (taking its {@link ByteBuffer#position()} into account). + * @return the memory address of this {@link ByteBuffer}s position. + */ + static long memoryAddressWithPosition(ByteBuffer buf) { + assert buf.isDirect(); + return buffer_memory_address(buf) + buf.position(); + } + + @SuppressWarnings("deprecation") + static ByteBuf allocateNativeOrder(int capacity) { + // Just use Unpooled as the life-time of these buffers is long. + ByteBuf buffer = Unpooled.directBuffer(capacity); + + // As we use the buffers as pointers to int etc we need to ensure we use the right oder so we will + // see the right value when we read primitive values. + return PlatformDependent.BIG_ENDIAN_NATIVE_ORDER ? buffer : buffer.order(ByteOrder.LITTLE_ENDIAN); + } + + static Exception newException(int err) { + final QuicError error = QuicError.valueOf(err); + final QuicException reason = new QuicException(error); + if (err == QUICHE_ERR_TLS_FAIL) { + String lastSslError = BoringSSL.ERR_last_error(); + final String message; + if (lastSslError != null) { + message = error.message() + ": " + lastSslError; + } else { + message = error.message(); + } + final SSLHandshakeException sslExc = new SSLHandshakeException(message); + sslExc.initCause(reason); + return sslExc; + } + if (err == QUICHE_ERR_CRYPTO_FAIL) { + return new SSLException(error.message(), reason); + } + return reason; + } + + static boolean shouldClose(int res) { + return res == Quiche.QUICHE_ERR_CRYPTO_FAIL || res == Quiche.QUICHE_ERR_TLS_FAIL; + } + + static boolean throwIfError(int res) throws Exception { + if (res < 0) { + if (res == Quiche.QUICHE_ERR_DONE) { + return true; + } + throw Quiche.newException(res); + } + return false; + } + + static void notifyPromise(int res, ChannelPromise promise) { + if (res < 0 && res != Quiche.QUICHE_ERR_DONE) { + promise.setFailure(Quiche.newException(res)); + } else { + promise.setSuccess(); + } + } + + /** + * Returns {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored. + * + * @param memory the first {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @param memory2 the second {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @return {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored, {@code false} + * otherwise. + */ + static boolean isSameAddress(ByteBuffer memory, ByteBuffer memory2, int addressOffset) { + long address1 = Quiche.memoryAddressWithPosition(memory) + addressOffset; + long address2 = Quiche.memoryAddressWithPosition(memory2) + addressOffset; + return SockaddrIn.cmp(address1, address2) == 0; + } + + static void setPrimitiveValue(ByteBuffer memory, int offset, int valueType, long value) { + switch (valueType) { + case 1: + memory.put(offset, (byte) value); + break; + case 2: + memory.putShort(offset, (short) value); + break; + case 4: + memory.putInt(offset, (int) value); + break; + case 8: + memory.putLong(offset, value); + break; + default: + throw new IllegalStateException(); + } + } + + static long getPrimitiveValue(ByteBuffer memory, int offset, int valueType) { + switch (valueType) { + case 1: + return memory.get(offset); + case 2: + return memory.getShort(offset); + case 4: + return memory.getInt(offset); + case 8: + return memory.getLong(offset); + default: + throw new IllegalStateException(); + } + } + + private Quiche() { } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheConfig.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheConfig.java new file mode 100644 index 0000000..c5866dc --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheConfig.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +final class QuicheConfig { + private final boolean isDatagramSupported; + private long config = -1; + + QuicheConfig(int version, Boolean grease, Long maxIdleTimeout, Long maxSendUdpPayloadSize, + Long maxRecvUdpPayloadSize, Long initialMaxData, + Long initialMaxStreamDataBidiLocal, Long initialMaxStreamDataBidiRemote, + Long initialMaxStreamDataUni, Long initialMaxStreamsBidi, Long initialMaxStreamsUni, + Long ackDelayExponent, Long maxAckDelay, Boolean disableActiveMigration, Boolean enableHystart, + QuicCongestionControlAlgorithm congestionControlAlgorithm, + Integer recvQueueLen, Integer sendQueueLen, + Long activeConnectionIdLimit, byte[] statelessResetToken) { + long config = Quiche.quiche_config_new(version); + try { + if (grease != null) { + Quiche.quiche_config_grease(config, grease); + } + if (maxIdleTimeout != null) { + Quiche.quiche_config_set_max_idle_timeout(config, maxIdleTimeout); + } + if (maxSendUdpPayloadSize != null) { + Quiche.quiche_config_set_max_send_udp_payload_size(config, maxSendUdpPayloadSize); + } + if (maxRecvUdpPayloadSize != null) { + Quiche.quiche_config_set_max_recv_udp_payload_size(config, maxRecvUdpPayloadSize); + } + if (initialMaxData != null) { + Quiche.quiche_config_set_initial_max_data(config, initialMaxData); + } + if (initialMaxStreamDataBidiLocal != null) { + Quiche.quiche_config_set_initial_max_stream_data_bidi_local(config, initialMaxStreamDataBidiLocal); + } + if (initialMaxStreamDataBidiRemote != null) { + Quiche.quiche_config_set_initial_max_stream_data_bidi_remote(config, initialMaxStreamDataBidiRemote); + } + if (initialMaxStreamDataUni != null) { + Quiche.quiche_config_set_initial_max_stream_data_uni(config, initialMaxStreamDataUni); + } + if (initialMaxStreamsBidi != null) { + Quiche.quiche_config_set_initial_max_streams_bidi(config, initialMaxStreamsBidi); + } + if (initialMaxStreamsUni != null) { + Quiche.quiche_config_set_initial_max_streams_uni(config, initialMaxStreamsUni); + } + if (ackDelayExponent != null) { + Quiche.quiche_config_set_ack_delay_exponent(config, ackDelayExponent); + } + if (maxAckDelay != null) { + Quiche.quiche_config_set_max_ack_delay(config, maxAckDelay); + } + if (disableActiveMigration != null) { + Quiche.quiche_config_set_disable_active_migration(config, disableActiveMigration); + } + if (enableHystart != null) { + Quiche.quiche_config_enable_hystart(config, enableHystart); + } + if (congestionControlAlgorithm != null) { + switch (congestionControlAlgorithm) { + case RENO: + Quiche.quiche_config_set_cc_algorithm(config, Quiche.QUICHE_CC_RENO); + break; + case CUBIC: + Quiche.quiche_config_set_cc_algorithm(config, Quiche.QUICHE_CC_CUBIC); + break; + case BBR: + Quiche.quiche_config_set_cc_algorithm(config, Quiche.QUICHE_CC_BBR); + break; + default: + throw new IllegalArgumentException( + "Unknown congestionControlAlgorithm: " + congestionControlAlgorithm); + } + } + if (recvQueueLen != null && sendQueueLen != null) { + isDatagramSupported = true; + Quiche.quiche_config_enable_dgram(config, true, recvQueueLen, sendQueueLen); + } else { + isDatagramSupported = false; + } + if (activeConnectionIdLimit != null) { + Quiche.quiche_config_set_active_connection_id_limit(config, activeConnectionIdLimit); + } + if (statelessResetToken != null) { + Quiche.quiche_config_set_stateless_reset_token(config, statelessResetToken); + } + this.config = config; + } catch (Throwable cause) { + Quiche.quiche_config_free(config); + throw cause; + } + } + + boolean isDatagramSupported() { + return isDatagramSupported; + } + + long nativeAddress() { + return config; + } + + // Let's override finalize() as we want to ensure we never leak memory even if the user will miss to close + // Channel that uses this handler that used the config and just let it get GC'ed. + @Override + protected void finalize() throws Throwable { + try { + free(); + } finally { + super.finalize(); + } + } + + void free() { + if (config != -1) { + try { + Quiche.quiche_config_free(config); + } finally { + config = -1; + } + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheLogger.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheLogger.java new file mode 100644 index 0000000..5e8ca06 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheLogger.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.util.internal.logging.InternalLogger; + +/** + * Delegates QUICHE logging to {@link InternalLogger}. + */ +final class QuicheLogger { + private final InternalLogger logger; + + QuicheLogger(InternalLogger logger) { + this.logger = logger; + } + + // Called from JNI. + @SuppressWarnings("unused") + void log(String msg) { + logger.debug(msg); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheNativeStaticallyReferencedJniMethods.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheNativeStaticallyReferencedJniMethods.java new file mode 100644 index 0000000..4183e29 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheNativeStaticallyReferencedJniMethods.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +final class QuicheNativeStaticallyReferencedJniMethods { + + static native int quiche_protocol_version(); + static native int quiche_max_conn_id_len(); + static native int quiche_shutdown_read(); + static native int quiche_shutdown_write(); + + static native int quiche_err_done(); + static native int quiche_err_buffer_too_short(); + static native int quiche_err_unknown_version(); + static native int quiche_err_invalid_frame(); + static native int quiche_err_invalid_packet(); + static native int quiche_err_invalid_state(); + static native int quiche_err_invalid_stream_state(); + static native int quiche_err_invalid_transport_param(); + static native int quiche_err_crypto_fail(); + static native int quiche_err_tls_fail(); + static native int quiche_err_flow_control(); + static native int quiche_err_stream_limit(); + static native int quiche_err_final_size(); + static native int quiche_err_stream_stopped(); + static native int quiche_err_stream_reset(); + static native int quiche_err_congestion_control(); + static native int quiche_err_id_limit(); + static native int quiche_err_out_of_identifiers(); + static native int quiche_err_key_update(); + + static native int quiche_cc_reno(); + static native int quiche_cc_cubic(); + static native int quiche_cc_bbr(); + + static native int quicheRecvInfoOffsetofFrom(); + static native int quicheRecvInfoOffsetofFromLen(); + static native int quicheRecvInfoOffsetofTo(); + static native int quicheRecvInfoOffsetofToLen(); + + static native int sizeofQuicheRecvInfo(); + static native int quicheSendInfoOffsetofTo(); + static native int quicheSendInfoOffsetofToLen(); + static native int quicheSendInfoOffsetofFrom(); + static native int quicheSendInfoOffsetofFromLen(); + + static native int quicheSendInfoOffsetofAt(); + + static native int sizeofQuicheSendInfo(); + + static native int afInet(); + static native int afInet6(); + static native int sizeofSockaddrIn(); + static native int sizeofSockaddrIn6(); + static native int sockaddrInOffsetofSinFamily(); + static native int sockaddrInOffsetofSinPort(); + static native int sockaddrInOffsetofSinAddr(); + static native int inAddressOffsetofSAddr(); + static native int sockaddrIn6OffsetofSin6Family(); + static native int sockaddrIn6OffsetofSin6Port(); + static native int sockaddrIn6OffsetofSin6Flowinfo(); + static native int sockaddrIn6OffsetofSin6Addr(); + static native int sockaddrIn6OffsetofSin6ScopeId(); + static native int in6AddressOffsetofS6Addr(); + static native int sizeofSockaddrStorage(); + static native int sizeofSocklenT(); + static native int sizeofSizeT(); + + static native int sizeofTimespec(); + static native int timespecOffsetofTvSec(); + static native int timespecOffsetofTvNsec(); + static native int sizeofTimeT(); + static native int sizeofLong(); + + static native int quiche_path_event_new(); + static native int quiche_path_event_validated(); + static native int quiche_path_event_failed_validation(); + static native int quiche_path_event_closed(); + static native int quiche_path_event_reused_source_connection_id(); + static native int quiche_path_event_peer_migrated(); + + private QuicheNativeStaticallyReferencedJniMethods() { } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannel.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannel.java new file mode 100644 index 0000000..4713c13 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannel.java @@ -0,0 +1,1969 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.AbstractChannel; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.DefaultChannelPipeline; +import io.netty.channel.EventLoop; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.ssl.SniCompletionEvent; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.netty.util.AttributeKey; +import io.netty.util.collection.LongObjectHashMap; +import io.netty.util.collection.LongObjectMap; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.ImmediateExecutor; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import java.io.File; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AlreadyConnectedException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ConnectionPendingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * {@link QuicChannel} implementation that uses quiche. + */ +final class QuicheQuicChannel extends AbstractChannel implements QuicChannel { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicheQuicChannel.class); + private static final String QLOG_FILE_EXTENSION = ".qlog"; + + enum StreamRecvResult { + /** + * Nothing more to read from the stream. + */ + DONE, + /** + * FIN flag received. + */ + FIN, + /** + * Normal read without FIN flag. + */ + OK + } + + private static final class CloseData implements ChannelFutureListener { + final boolean applicationClose; + final int err; + final ByteBuf reason; + + CloseData(boolean applicationClose, int err, ByteBuf reason) { + this.applicationClose = applicationClose; + this.err = err; + this.reason = reason; + } + + @Override + public void operationComplete(ChannelFuture future) { + reason.release(); + } + } + + private final ChannelFutureListener continueSendingListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture channelFuture) { + if (connectionSend()) { + flushParent(); + } + } + }; + + private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); + private final long[] readableStreams = new long[128]; + private final long[] writableStreams = new long[128]; + + private final LongObjectMap streams = new LongObjectHashMap<>(); + private final QuicheQuicChannelConfig config; + private final boolean server; + private final QuicStreamIdGenerator idGenerator; + private final ChannelHandler streamHandler; + private final Map.Entry, Object>[] streamOptionsArray; + private final Map.Entry, Object>[] streamAttrsArray; + private final TimeoutHandler timeoutHandler; + private Executor sslTaskExecutor; + + private boolean inFireChannelReadCompleteQueue; + private boolean fireChannelReadCompletePending; + private ByteBuf finBuffer; + private ChannelPromise connectPromise; + private ScheduledFuture connectTimeoutFuture; + private QuicConnectionAddress connectAddress; + private final Set sourceConnectionIds = new HashSet<>(); + private CloseData closeData; + private QuicConnectionCloseEvent connectionCloseEvent; + private QuicConnectionStats statsAtClose; + + private InetSocketAddress local; + private InetSocketAddress remote; + private boolean supportsDatagram; + private boolean recvDatagramPending; + private boolean datagramReadable; + + private boolean recvStreamPending; + private boolean streamReadable; + private boolean handshakeCompletionNotified; + private boolean earlyDataReadyNotified; + + private int reantranceGuard = 0; + private static final int IN_RECV = 1 << 1; + private static final int IN_CONNECTION_SEND = 1 << 2; + private static final int IN_HANDLE_WRITABLE_STREAMS = 1 << 3; + private static final int IN_FORCE_CLOSE = 1 << 4; + + private static final int CLOSED = 0; + private static final int OPEN = 1; + private static final int ACTIVE = 2; + private volatile int state; + private volatile boolean timedOut; + private volatile String traceId; + private volatile QuicheQuicConnection connection; + + private static final AtomicLongFieldUpdater UNI_STREAMS_LEFT_UPDATER = + AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "uniStreamsLeft"); + private volatile long uniStreamsLeft; + + private static final AtomicLongFieldUpdater BIDI_STREAMS_LEFT_UPDATER = + AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "bidiStreamsLeft"); + private volatile long bidiStreamsLeft; + + private QuicheQuicChannel(Channel parent, boolean server, ByteBuffer key, InetSocketAddress local, + InetSocketAddress remote, boolean supportsDatagram, ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray, + Consumer timeoutTask, + Executor sslTaskExecutor) { + super(parent); + config = new QuicheQuicChannelConfig(this); + this.server = server; + this.idGenerator = new QuicStreamIdGenerator(server); + if (key != null) { + this.sourceConnectionIds.add(key); + } + state = OPEN; + + this.supportsDatagram = supportsDatagram; + this.local = local; + this.remote = remote; + + this.streamHandler = streamHandler; + this.streamOptionsArray = streamOptionsArray; + this.streamAttrsArray = streamAttrsArray; + timeoutHandler = new TimeoutHandler(timeoutTask); + this.sslTaskExecutor = sslTaskExecutor == null ? ImmediateExecutor.INSTANCE : sslTaskExecutor; + } + + static QuicheQuicChannel forClient(Channel parent, InetSocketAddress local, InetSocketAddress remote, + ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray) { + return new QuicheQuicChannel(parent, false, null, local, remote, false, streamHandler, + streamOptionsArray, streamAttrsArray, null, null); + } + + static QuicheQuicChannel forServer(Channel parent, ByteBuffer key, InetSocketAddress local, + InetSocketAddress remote, + boolean supportsDatagram, ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray, + Consumer timeoutTask, Executor sslTaskExecutor) { + return new QuicheQuicChannel(parent, true, key, local, remote, supportsDatagram, + streamHandler, streamOptionsArray, streamAttrsArray, timeoutTask, + sslTaskExecutor); + } + + @Override + public boolean isTimedOut() { + return timedOut; + } + + @Override + public SSLEngine sslEngine() { + QuicheQuicConnection connection = this.connection; + return connection == null ? null : connection.engine(); + } + + private void notifyAboutHandshakeCompletionIfNeeded(SSLHandshakeException cause) { + if (handshakeCompletionNotified) { + return; + } + if (cause != null) { + pipeline().fireUserEventTriggered(new SslHandshakeCompletionEvent(cause)); + return; + } + QuicheQuicConnection connection = this.connection; + if (connection == null) { + return; + } + switch (connection.engine().getHandshakeStatus()) { + case NOT_HANDSHAKING: + case FINISHED: + handshakeCompletionNotified = true; + String sniHostname = connection.engine().sniHostname; + if (sniHostname != null) { + connection.engine().sniHostname = null; + pipeline().fireUserEventTriggered(new SniCompletionEvent(sniHostname)); + } + pipeline().fireUserEventTriggered(SslHandshakeCompletionEvent.SUCCESS); + break; + default: + break; + } + } + + @Override + public long peerAllowedStreams(QuicStreamType type) { + switch (type) { + case BIDIRECTIONAL: + return bidiStreamsLeft; + case UNIDIRECTIONAL: + return uniStreamsLeft; + default: + return 0; + } + } + + void attachQuicheConnection(QuicheQuicConnection connection) { + this.connection = connection; + + byte[] traceId = Quiche.quiche_conn_trace_id(connection.address()); + if (traceId != null) { + this.traceId = new String(traceId); + } + + connection.initInfo(local, remote); + + // Setup QLOG if needed. + QLogConfiguration configuration = config.getQLogConfiguration(); + if (configuration != null) { + final String fileName; + File file = new File(configuration.path()); + if (file.isDirectory()) { + // Create directory if needed. + file.mkdir(); + if (this.traceId != null) { + fileName = configuration.path() + File.separatorChar + this.traceId + "-" + + id().asShortText() + QLOG_FILE_EXTENSION; + } else { + fileName = configuration.path() + File.separatorChar + id().asShortText() + QLOG_FILE_EXTENSION; + } + } else { + fileName = configuration.path(); + } + + if (!Quiche.quiche_conn_set_qlog_path(connection.address(), fileName, + configuration.logTitle(), configuration.logDescription())) { + logger.info("Unable to create qlog file: {} ", fileName); + } + } + } + + private void connect(Function engineProvider, Executor sslTaskExecutor, + long configAddr, int localConnIdLength, + boolean supportsDatagram, ByteBuffer fromSockaddrMemory, ByteBuffer toSockaddrMemory) + throws Exception { + assert this.connection == null; + assert this.traceId == null; + assert this.sourceConnectionIds.isEmpty(); + + this.sslTaskExecutor = sslTaskExecutor; + + QuicConnectionAddress address = this.connectAddress; + if (address == QuicConnectionAddress.EPHEMERAL) { + address = QuicConnectionAddress.random(localConnIdLength); + } else { + if (address.connId.remaining() != localConnIdLength) { + failConnectPromiseAndThrow(new IllegalArgumentException("connectionAddress has length " + + address.connId.remaining() + + " instead of " + localConnIdLength)); + } + } + QuicSslEngine engine = engineProvider.apply(this); + if (!(engine instanceof QuicheQuicSslEngine)) { + failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not of type " + + QuicheQuicSslEngine.class.getSimpleName())); + return; + } + if (!engine.getUseClientMode()) { + failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not create in client mode")); + } + QuicheQuicSslEngine quicheEngine = (QuicheQuicSslEngine) engine; + ByteBuffer connectId = address.connId.duplicate(); + ByteBuf idBuffer = alloc().directBuffer(connectId.remaining()).writeBytes(connectId.duplicate()); + try { + int fromSockaddrLen = SockaddrIn.setAddress(fromSockaddrMemory, local); + int toSockaddrLen = SockaddrIn.setAddress(toSockaddrMemory, remote); + QuicheQuicConnection connection = quicheEngine.createConnection(ssl -> + Quiche.quiche_conn_new_with_tls(Quiche.readerMemoryAddress(idBuffer), + idBuffer.readableBytes(), -1, -1, + Quiche.memoryAddressWithPosition(fromSockaddrMemory), fromSockaddrLen, + Quiche.memoryAddressWithPosition(toSockaddrMemory), toSockaddrLen, + configAddr, ssl, false)); + if (connection == null) { + failConnectPromiseAndThrow(new ConnectException()); + 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; + sourceConnectionIds.add(connectId); + } finally { + idBuffer.release(); + } + } + + private void failConnectPromiseAndThrow(Exception e) throws Exception { + tryFailConnectPromise(e); + throw e; + } + + private boolean tryFailConnectPromise(Exception e) { + ChannelPromise promise = connectPromise; + if (promise != null) { + connectPromise = null; + promise.tryFailure(e); + return true; + } + return false; + } + + Set sourceConnectionIds() { + return sourceConnectionIds; + } + + private boolean closeAllIfConnectionClosed() { + if (connection.isClosed()) { + forceClose(); + return true; + } + return false; + } + + boolean markInFireChannelReadCompleteQueue() { + if (inFireChannelReadCompleteQueue) { + return false; + } + inFireChannelReadCompleteQueue = true; + return true; + } + + private void failPendingConnectPromise() { + ChannelPromise promise = QuicheQuicChannel.this.connectPromise; + if (promise != null) { + QuicheQuicChannel.this.connectPromise = null; + promise.tryFailure(new QuicClosedChannelException(this.connectionCloseEvent)); + } + } + + void forceClose() { + if (isConnDestroyed() || (reantranceGuard & IN_FORCE_CLOSE) != 0) { + // Just return if we already destroyed the underlying connection. + return; + } + reantranceGuard |= IN_FORCE_CLOSE; + + QuicheQuicConnection conn = connection; + + unsafe().close(voidPromise()); + // making sure that connection statistics is avaliable + // even after channel is closed + statsAtClose = collectStats0(conn, eventLoop().newPromise()); + try { + failPendingConnectPromise(); + state = CLOSED; + timedOut = Quiche.quiche_conn_is_timed_out(conn.address()); + + closeStreams(); + + if (finBuffer != null) { + finBuffer.release(); + finBuffer = null; + } + state = CLOSED; + + timeoutHandler.cancel(); + } finally { + flushParent(); + connection = null; + conn.free(); + } + } + + @Override + protected DefaultChannelPipeline newChannelPipeline() { + return new DefaultChannelPipeline(this) { + @Override + protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof QuicStreamChannel) { + QuicStreamChannel channel = (QuicStreamChannel) msg; + Quic.setupChannel(channel, streamOptionsArray, streamAttrsArray, streamHandler, logger); + ctx.channel().eventLoop().register(channel); + } else { + super.onUnhandledInboundMessage(ctx, msg); + } + } + }; + } + + @Override + public QuicChannel flush() { + super.flush(); + return this; + } + + @Override + public QuicChannel read() { + super.read(); + return this; + } + + @Override + public Future createStream(QuicStreamType type, ChannelHandler handler, + Promise promise) { + if (eventLoop().inEventLoop()) { + ((QuicChannelUnsafe) unsafe()).connectStream(type, handler, promise); + } else { + eventLoop().execute(() -> ((QuicChannelUnsafe) unsafe()).connectStream(type, handler, promise)); + } + return promise; + } + + @Override + public ChannelFuture close(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + close0(applicationClose, error, reason, promise); + } else { + eventLoop().execute(() -> close0(applicationClose, error, reason, promise)); + } + return promise; + } + + private void close0(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise) { + if (closeData == null) { + if (!reason.hasMemoryAddress()) { + // Copy to direct buffer as that's what we need. + ByteBuf copy = alloc().directBuffer(reason.readableBytes()).writeBytes(reason); + reason.release(); + reason = copy; + } + closeData = new CloseData(applicationClose, error, reason); + promise.addListener(closeData); + } else { + // We already have a close scheduled that uses a close data. Lets release the buffer early. + reason.release(); + } + close(promise); + } + + @Override + public String toString() { + String traceId = this.traceId; + if (traceId == null) { + return "()" + super.toString(); + } else { + return '(' + traceId + ')' + super.toString(); + } + } + + @Override + protected AbstractUnsafe newUnsafe() { + return new QuicChannelUnsafe(); + } + + @Override + protected boolean isCompatible(EventLoop eventLoop) { + return parent().eventLoop() == eventLoop; + } + + @Override + protected SocketAddress localAddress0() { + QuicheQuicConnection connection = this.connection; + return connection == null ? null : connection.sourceId(); + } + + @Override + protected SocketAddress remoteAddress0() { + QuicheQuicConnection connection = this.connection; + return connection == null ? null : connection.destinationId(); + } + + @Override + protected void doBind(SocketAddress socketAddress) { + throw new UnsupportedOperationException(); + } + + @Override + protected void doDisconnect() throws Exception { + doClose(); + } + + @Override + protected void doClose() throws Exception { + state = CLOSED; + + final boolean app; + final int err; + final ByteBuf reason; + if (closeData == null) { + app = false; + err = 0; + reason = Unpooled.EMPTY_BUFFER; + } else { + app = closeData.applicationClose; + err = closeData.err; + reason = closeData.reason; + closeData = null; + } + + // Call connectionSend() so we ensure we send all that is queued before we close the channel + boolean written = connectionSend(); + + failPendingConnectPromise(); + Quiche.throwIfError(Quiche.quiche_conn_close(connectionAddressChecked(), app, err, + Quiche.readerMemoryAddress(reason), reason.readableBytes())); + + // As we called quiche_conn_close(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.close + written |= connectionSend(); + if (written) { + // As this is the close let us flush it asap. + forceFlushParent(); + } + } + + @Override + protected void doBeginRead() { + recvDatagramPending = true; + recvStreamPending = true; + if (datagramReadable || streamReadable) { + ((QuicChannelUnsafe) unsafe()).recv(); + } + } + + @Override + protected Object filterOutboundMessage(Object msg) { + if (msg instanceof ByteBuf) { + return msg; + } + throw new UnsupportedOperationException("Unsupported message type: " + StringUtil.simpleClassName(msg)); + } + + @Override + protected void doWrite(ChannelOutboundBuffer channelOutboundBuffer) throws Exception { + if (!supportsDatagram) { + throw new UnsupportedOperationException("Datagram extension is not supported"); + } + boolean sendSomething = false; + boolean retry = false; + try { + for (;;) { + ByteBuf buffer = (ByteBuf) channelOutboundBuffer.current(); + if (buffer == null) { + break; + } + + int readable = buffer.readableBytes(); + if (readable == 0) { + // Skip empty buffers. + channelOutboundBuffer.remove(); + continue; + } + + final int res; + if (!buffer.isDirect() || buffer.nioBufferCount() > 1) { + ByteBuf tmpBuffer = alloc().directBuffer(readable); + try { + tmpBuffer.writeBytes(buffer, buffer.readerIndex(), readable); + res = sendDatagram(tmpBuffer); + } finally { + tmpBuffer.release(); + } + } else { + res = sendDatagram(buffer); + } + if (res >= 0) { + channelOutboundBuffer.remove(); + sendSomething = true; + retry = false; + } else { + if (res == Quiche.QUICHE_ERR_BUFFER_TOO_SHORT) { + retry = false; + channelOutboundBuffer.remove(Quiche.newException(res)); + } else if (res == Quiche.QUICHE_ERR_INVALID_STATE) { + throw new UnsupportedOperationException("Remote peer does not support Datagram extension", + Quiche.newException(res)); + } else if (Quiche.throwIfError(res)) { + if (retry) { + // We already retried and it didn't work. Let's drop the datagrams on the floor. + for (;;) { + if (!channelOutboundBuffer.remove()) { + // The buffer is empty now. + return; + } + } + } + // Set sendSomething to false a we will call connectionSend() now. + sendSomething = false; + // If this returned DONE we couldn't write anymore. This happens if the internal queue + // is full. In this case we should call quiche_conn_send(...) and so make space again. + if (connectionSend()) { + forceFlushParent(); + } + // Let's try again to write the message. + retry = true; + } + } + } + } finally { + if (sendSomething && connectionSend()) { + flushParent(); + } + } + } + + private int sendDatagram(ByteBuf buf) throws ClosedChannelException { + return Quiche.quiche_conn_dgram_send(connectionAddressChecked(), + Quiche.readerMemoryAddress(buf), buf.readableBytes()); + } + + @Override + public QuicChannelConfig config() { + return config; + } + + @Override + public boolean isOpen() { + return state >= OPEN; + } + + @Override + public boolean isActive() { + return state == ACTIVE; + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + /** + * This may call {@link #flush()} on the parent channel if needed. The flush may delayed until the read loop + * is over. + */ + private void flushParent() { + if (!inFireChannelReadCompleteQueue) { + forceFlushParent(); + } + } + + /** + * Call @link #flush()} on the parent channel. + */ + private void forceFlushParent() { + parent().flush(); + } + + private long connectionAddressChecked() throws ClosedChannelException { + if (isConnDestroyed()) { + throw new ClosedChannelException(); + } + return connection.address(); + } + + boolean freeIfClosed() { + if (isConnDestroyed()) { + return true; + } + return closeAllIfConnectionClosed(); + } + + private void closeStreams() { + // Make a copy to ensure we not run into a situation when we change the underlying iterator from + // another method and so run in an assert error. + for (QuicheQuicStreamChannel stream: streams.values().toArray(new QuicheQuicStreamChannel[0])) { + stream.unsafe().close(voidPromise()); + } + streams.clear(); + } + + void streamPriority(long streamId, byte priority, boolean incremental) throws Exception { + Quiche.throwIfError(Quiche.quiche_conn_stream_priority(connectionAddressChecked(), streamId, + priority, incremental)); + } + + void streamClosed(long streamId) { + streams.remove(streamId); + } + + boolean isStreamLocalCreated(long streamId) { + return (streamId & 0x1) == (server ? 1 : 0); + } + + QuicStreamType streamType(long streamId) { + return (streamId & 0x2) == 0 ? QuicStreamType.BIDIRECTIONAL : QuicStreamType.UNIDIRECTIONAL; + } + + void streamShutdown(long streamId, boolean read, boolean write, int err, ChannelPromise promise) { + final long connectionAddress; + try { + connectionAddress = connectionAddressChecked(); + } catch (ClosedChannelException e) { + promise.setFailure(e); + return; + } + int res = 0; + if (read) { + res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_READ, err); + } + if (write) { + res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_WRITE, err); + } + + // As we called quiche_conn_stream_shutdown(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + if (connectionSend()) { + // Force the flush so the shutdown can be seen asap. + forceFlushParent(); + } + Quiche.notifyPromise(res, promise); + } + + void streamSendFin(long streamId) throws Exception { + try { + // Just write an empty buffer and set fin to true. + Quiche.throwIfError(streamSend0(streamId, Unpooled.EMPTY_BUFFER, true)); + } finally { + // As we called quiche_conn_stream_send(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + if (connectionSend()) { + flushParent(); + } + } + } + + int streamSend(long streamId, ByteBuf buffer, boolean fin) throws ClosedChannelException { + if (buffer.nioBufferCount() == 1) { + return streamSend0(streamId, buffer, fin); + } + ByteBuffer[] nioBuffers = buffer.nioBuffers(); + int lastIdx = nioBuffers.length - 1; + int res = 0; + for (int i = 0; i < lastIdx; i++) { + ByteBuffer nioBuffer = nioBuffers[i]; + while (nioBuffer.hasRemaining()) { + int localRes = streamSend(streamId, nioBuffer, false); + if (localRes <= 0) { + return res; + } + res += localRes; + + nioBuffer.position(nioBuffer.position() + localRes); + } + } + int localRes = streamSend(streamId, nioBuffers[lastIdx], fin); + if (localRes > 0) { + res += localRes; + } + return res; + } + + void connectionSendAndFlush() { + if (inFireChannelReadCompleteQueue || (reantranceGuard & IN_HANDLE_WRITABLE_STREAMS) != 0) { + return; + } + if (connectionSend()) { + flushParent(); + } + } + + private int streamSend0(long streamId, ByteBuf buffer, boolean fin) throws ClosedChannelException { + return Quiche.quiche_conn_stream_send(connectionAddressChecked(), streamId, + Quiche.readerMemoryAddress(buffer), buffer.readableBytes(), fin); + } + + private int streamSend(long streamId, ByteBuffer buffer, boolean fin) throws ClosedChannelException { + return Quiche.quiche_conn_stream_send(connectionAddressChecked(), streamId, + Quiche.memoryAddressWithPosition(buffer), buffer.remaining(), fin); + } + + StreamRecvResult streamRecv(long streamId, ByteBuf buffer) throws Exception { + if (finBuffer == null) { + finBuffer = alloc().directBuffer(1); + } + int writerIndex = buffer.writerIndex(); + int recvLen = Quiche.quiche_conn_stream_recv(connectionAddressChecked(), streamId, + Quiche.writerMemoryAddress(buffer), buffer.writableBytes(), Quiche.writerMemoryAddress(finBuffer)); + if (Quiche.throwIfError(recvLen)) { + return StreamRecvResult.DONE; + } + + buffer.writerIndex(writerIndex + recvLen); + return finBuffer.getBoolean(0) ? StreamRecvResult.FIN : StreamRecvResult.OK; + } + + /** + * Receive some data on a QUIC connection. + */ + void recv(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) { + ((QuicChannelUnsafe) unsafe()).connectionRecv(sender, recipient, buffer); + } + + /** + * Return all source connection ids that are retired and so should be removed to map to the channel. + * + * @return retired ids. + */ + List retiredSourceConnectionId() { + QuicheQuicConnection connection = this.connection; + if (connection == null || connection.isClosed()) { + return Collections.emptyList(); + } + long connAddr = connection.address(); + assert connAddr != -1; + List retiredSourceIds = null; + for (;;) { + byte[] retired = Quiche.quiche_conn_retired_scid_next(connAddr); + if (retired == null) { + break; + } + if (retiredSourceIds == null) { + retiredSourceIds = new ArrayList<>(); + } + ByteBuffer retiredId = ByteBuffer.wrap(retired); + retiredSourceIds.add(retiredId); + sourceConnectionIds.remove(retiredId); + } + if (retiredSourceIds == null) { + return Collections.emptyList(); + } + return retiredSourceIds; + } + + List newSourceConnectionIds( + QuicConnectionIdGenerator connectionIdGenerator, QuicResetTokenGenerator resetTokenGenerator) { + if (server) { + QuicheQuicConnection connection = this.connection; + if (connection == null || connection.isClosed()) { + return Collections.emptyList(); + } + long connAddr = connection.address(); + // Generate all extra source ids that we can provide. This will cause frames that need to be send. Which + // is the reason why we might need to call connectionSendAndFlush(). + int left = Quiche.quiche_conn_scids_left(connAddr); + if (left > 0) { + QuicConnectionAddress sourceAddr = connection.sourceId(); + if (sourceAddr == null) { + return Collections.emptyList(); + } + List generatedIds = new ArrayList<>(left); + boolean sendAndFlush = false; + ByteBuffer key = sourceAddr.connId.duplicate(); + ByteBuf connIdBuffer = alloc().directBuffer(key.remaining()); + + byte[] resetTokenArray = new byte[Quic.RESET_TOKEN_LEN]; + try { + do { + ByteBuffer srcId = connectionIdGenerator.newId(key, key.remaining()); + connIdBuffer.clear(); + connIdBuffer.writeBytes(srcId.duplicate()); + ByteBuffer resetToken = resetTokenGenerator.newResetToken(srcId.duplicate()); + resetToken.get(resetTokenArray); + long result = Quiche.quiche_conn_new_scid( + connAddr, Quiche.memoryAddress(connIdBuffer, 0, connIdBuffer.readableBytes()), + connIdBuffer.readableBytes(), resetTokenArray, false, -1); + if (result < 0) { + break; + } + sendAndFlush = true; + generatedIds.add(srcId); + sourceConnectionIds.add(srcId); + } while (--left > 0); + } finally { + connIdBuffer.release(); + } + + if (sendAndFlush) { + connectionSendAndFlush(); + } + return generatedIds; + } + } + return Collections.emptyList(); + } + + void writable() { + boolean written = connectionSend(); + handleWritableStreams(); + written |= connectionSend(); + + if (written) { + // The writability changed so lets flush as fast as possible. + forceFlushParent(); + } + } + + int streamCapacity(long streamId) { + if (connection.isClosed()) { + return 0; + } + return Quiche.quiche_conn_stream_capacity(connection.address(), streamId); + } + + private boolean handleWritableStreams() { + if (isConnDestroyed()) { + return false; + } + reantranceGuard |= IN_HANDLE_WRITABLE_STREAMS; + try { + long connAddr = connection.address(); + boolean mayNeedWrite = false; + + if (Quiche.quiche_conn_is_established(connAddr) || + Quiche.quiche_conn_is_in_early_data(connAddr)) { + long writableIterator = Quiche.quiche_conn_writable(connAddr); + + try { + // For streams we always process all streams when at least on read was requested. + for (;;) { + int writable = Quiche.quiche_stream_iter_next( + writableIterator, writableStreams); + for (int i = 0; i < writable; i++) { + long streamId = writableStreams[i]; + QuicheQuicStreamChannel streamChannel = streams.get(streamId); + if (streamChannel != null) { + int capacity = Quiche.quiche_conn_stream_capacity(connAddr, streamId); + if (capacity < 0) { + // Let's close the channel if quiche_conn_stream_capacity(...) returns an error. + streamChannel.forceClose(); + } else if (streamChannel.writable(capacity)) { + mayNeedWrite = true; + } + } + } + if (writable < writableStreams.length) { + // We did handle all writable streams. + break; + } + } + } finally { + Quiche.quiche_stream_iter_free(writableIterator); + } + } + return mayNeedWrite; + } finally { + reantranceGuard &= ~IN_HANDLE_WRITABLE_STREAMS; + } + } + + /** + * Called once we receive a channelReadComplete event. This method will take care of calling + * {@link ChannelPipeline#fireChannelReadComplete()} if needed and also to handle pending flushes of + * writable {@link QuicheQuicStreamChannel}s. + */ + void recvComplete() { + try { + if (isConnDestroyed()) { + // Ensure we flush all pending writes. + forceFlushParent(); + return; + } + fireChannelReadCompleteIfNeeded(); + + // If we had called recv we need to ensure we call send as well. + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + connectionSend(); + + // We are done with the read loop, flush all pending writes now. + forceFlushParent(); + } finally { + inFireChannelReadCompleteQueue = false; + } + } + + private void fireChannelReadCompleteIfNeeded() { + if (fireChannelReadCompletePending) { + fireChannelReadCompletePending = false; + pipeline().fireChannelReadComplete(); + } + } + + private boolean isConnDestroyed() { + return connection == null; + } + + private void fireExceptionEvents(Throwable cause) { + if (cause instanceof SSLHandshakeException) { + notifyAboutHandshakeCompletionIfNeeded((SSLHandshakeException) cause); + } + pipeline().fireExceptionCaught(cause); + } + + private boolean runTasksDirectly() { + return sslTaskExecutor == null || sslTaskExecutor == ImmediateExecutor.INSTANCE || + sslTaskExecutor == ImmediateEventExecutor.INSTANCE; + } + + private void runAllTaskSend(Runnable task) { + sslTaskExecutor.execute(decorateTaskSend(task)); + } + + private void runAll(Runnable task) { + do { + task.run(); + } while ((task = connection.sslTask()) != null); + } + + private Runnable decorateTaskSend(Runnable task) { + return () -> { + try { + runAll(task); + } finally { + // Move back to the EventLoop. + eventLoop().execute(() -> { + // Call connection send to continue handshake if needed. + if (connectionSend()) { + forceFlushParent(); + } + }); + } + }; + } + + private boolean connectionSendSegments(SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator) { + List bufferList = new ArrayList<>(segmentedDatagramPacketAllocator.maxNumSegments()); + long connAddr = connection.address(); + int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr); + boolean packetWasWritten = false; + boolean close = false; + try { + for (;;) { + int len = calculateSendBufferLength(connAddr, maxDatagramSize); + ByteBuf out = alloc().directBuffer(len); + + ByteBuffer sendInfo = connection.nextSendInfo(); + InetSocketAddress sendToAddress = this.remote; + + boolean done; + int writerIndex = out.writerIndex(); + int written = Quiche.quiche_conn_send( + connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(), + Quiche.memoryAddressWithPosition(sendInfo)); + if (written == 0) { + out.release(); + // No need to create a new datagram packet. Just try again. + continue; + } + + try { + done = Quiche.throwIfError(written); + } catch (Exception e) { + done = true; + close = Quiche.shouldClose(written); + if (!tryFailConnectPromise(e)) { + // Only fire through the pipeline if this does not fail the connect promise. + fireExceptionEvents(e); + } + } + int size = bufferList.size(); + if (done) { + // We are done, release the buffer and send what we did build up so far. + out.release(); + + switch (size) { + case 0: + // Nothing more to write. + break; + case 1: + // We can write a normal datagram packet. + parent().write(new DatagramPacket(bufferList.get(0), sendToAddress)); + packetWasWritten = true; + break; + default: + int segmentSize = segmentSize(bufferList); + ByteBuf compositeBuffer = Unpooled.wrappedBuffer(bufferList.toArray(new ByteBuf[0])); + // We had more than one buffer, create a segmented packet. + parent().write(segmentedDatagramPacketAllocator.newPacket( + compositeBuffer, segmentSize, sendToAddress)); + packetWasWritten = true; + break; + } + bufferList.clear(); + return packetWasWritten; + } + out.writerIndex(writerIndex + written); + + int segmentSize = -1; + if (connection.isSendInfoChanged()) { + // Change the cached address and let the user know there was a connection migration. + InetSocketAddress oldRemote = remote; + remote = QuicheSendInfo.getToAddress(sendInfo); + local = QuicheSendInfo.getFromAddress(sendInfo); + + if (size > 0) { + // We have something in the out list already, we need to send this now and so we set the + // segmentSize. + segmentSize = segmentSize(bufferList); + } + } else if (size > 0) { + int lastReadable = segmentSize(bufferList); + // Check if we either need to send now because the last buffer we added has a smaller size then this + // one or if we reached the maximum number of segments that we can send. + if (lastReadable != out.readableBytes() || + size == segmentedDatagramPacketAllocator.maxNumSegments()) { + segmentSize = lastReadable; + } + } + + // If the segmentSize is not -1 we know we need to send now what was in the out list. + if (segmentSize != -1) { + final boolean stop; + if (size == 1) { + // Only one buffer in the out list, there is no need to use segments. + stop = writePacket(new DatagramPacket( + bufferList.get(0), sendToAddress), maxDatagramSize, len); + } else { + // Create a packet with segments in. + ByteBuf compositeBuffer = Unpooled.wrappedBuffer(bufferList.toArray(new ByteBuf[0])); + stop = writePacket(segmentedDatagramPacketAllocator.newPacket( + compositeBuffer, segmentSize, sendToAddress), maxDatagramSize, len); + } + bufferList.clear(); + packetWasWritten = true; + + if (stop) { + // Nothing left in the window, continue later. That said we still need to also + // write the previous filled out buffer as otherwise we would either leak or need + // to drop it and so produce some loss. + if (out.isReadable()) { + parent().write(new DatagramPacket(out, sendToAddress)); + } else { + out.release(); + } + return true; + } + } + // Let's add a touch with the bufferList as a hint. This will help us to debug leaks if there + // are any. + out.touch(bufferList); + // store for later, so we can make use of segments. + bufferList.add(out); + } + } finally { + if (close) { + // Close now... now way to recover. + unsafe().close(newPromise()); + } + } + } + + private static int segmentSize(List bufferList) { + assert !bufferList.isEmpty(); + int size = bufferList.size(); + return bufferList.get(size - 1).readableBytes(); + } + + private boolean connectionSendSimple() { + long connAddr = connection.address(); + boolean packetWasWritten = false; + boolean close = false; + int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr); + for (;;) { + ByteBuffer sendInfo = connection.nextSendInfo(); + + int len = calculateSendBufferLength(connAddr, maxDatagramSize); + ByteBuf out = alloc().directBuffer(len); + int writerIndex = out.writerIndex(); + + int written = Quiche.quiche_conn_send( + connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(), + Quiche.memoryAddressWithPosition(sendInfo)); + + try { + if (Quiche.throwIfError(written)) { + out.release(); + break; + } + } catch (Exception e) { + close = Quiche.shouldClose(written); + out.release(); + if (!tryFailConnectPromise(e)) { + fireExceptionEvents(e); + } + break; + } + + if (written == 0) { + // No need to create a new datagram packet. Just release and try again. + out.release(); + continue; + } + if (connection.isSendInfoChanged()) { + // Change the cached address + remote = QuicheSendInfo.getToAddress(sendInfo); + local = QuicheSendInfo.getFromAddress(sendInfo); + } + out.writerIndex(writerIndex + written); + boolean stop = writePacket(new DatagramPacket(out, remote), maxDatagramSize, len); + packetWasWritten = true; + if (stop) { + // Nothing left in the window, continue later + break; + } + } + if (close) { + // Close now... now way to recover. + unsafe().close(newPromise()); + } + return packetWasWritten; + } + + private boolean writePacket(DatagramPacket packet, int maxDatagramSize, int len) { + ChannelFuture future = parent().write(packet); + if (isSendWindowUsed(maxDatagramSize, len)) { + // Nothing left in the window, continue later + future.addListener(continueSendingListener); + return true; + } + return false; + } + + private static boolean isSendWindowUsed(int maxDatagramSize, int len) { + return len < maxDatagramSize; + } + + private static int calculateSendBufferLength(long connAddr, int maxDatagramSize) { + int len = Math.min(maxDatagramSize, Quiche.quiche_conn_send_quantum(connAddr)); + if (len <= 0) { + // If there is no room left we just return some small number to reduce the risk of packet drop + // while still be able to attach the listener to the write future. + // We use the value of 8 because such an allocation will be cheap to serve from the + // PooledByteBufAllocator while still serve our need. + return 8; + } + return len; + } + + /** + * Write datagrams if needed and return {@code true} if something was written and we need to call + * {@link Channel#flush()} at some point. + */ + private boolean connectionSend() { + if (isConnDestroyed()) { + return false; + } + if ((reantranceGuard & IN_CONNECTION_SEND) != 0) { + // Let's notify about early data if needed. + notifyEarlyDataReadyIfNeeded(); + return false; + } + + reantranceGuard |= IN_CONNECTION_SEND; + try { + boolean packetWasWritten; + SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator = + config.getSegmentedDatagramPacketAllocator(); + if (segmentedDatagramPacketAllocator.maxNumSegments() > 0) { + packetWasWritten = connectionSendSegments(segmentedDatagramPacketAllocator); + } else { + packetWasWritten = connectionSendSimple(); + } + + // Process / schedule all tasks that were created. + Runnable task = connection.sslTask(); + if (task != null) { + if (runTasksDirectly()) { + // Consume all tasks + do { + task.run(); + // Notify about early data ready if needed. + notifyEarlyDataReadyIfNeeded(); + } while ((task = connection.sslTask()) != null); + + // Let's try again sending after we did process all tasks. + return packetWasWritten | connectionSend(); + } else { + runAllTaskSend(task); + } + } else { + // Notify about early data ready if needed. + notifyEarlyDataReadyIfNeeded(); + } + + if (packetWasWritten) { + timeoutHandler.scheduleTimeout(); + } + return packetWasWritten; + } finally { + reantranceGuard &= ~IN_CONNECTION_SEND; + } + } + + private final class QuicChannelUnsafe extends AbstractChannel.AbstractUnsafe { + + void connectStream(QuicStreamType type, ChannelHandler handler, + Promise promise) { + long streamId = idGenerator.nextStreamId(type == QuicStreamType.BIDIRECTIONAL); + try { + Quiche.throwIfError(streamSend0(streamId, Unpooled.EMPTY_BUFFER, false)); + } catch (Exception e) { + promise.setFailure(e); + return; + } + if (type == QuicStreamType.UNIDIRECTIONAL) { + UNI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this); + } else { + BIDI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this); + } + QuicheQuicStreamChannel streamChannel = addNewStreamChannel(streamId); + if (handler != null) { + streamChannel.pipeline().addLast(handler); + } + eventLoop().register(streamChannel).addListener((ChannelFuture f) -> { + if (f.isSuccess()) { + promise.setSuccess(streamChannel); + } else { + promise.setFailure(f.cause()); + streams.remove(streamId); + } + }); + } + + @Override + public void connect(SocketAddress remote, SocketAddress local, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + if (server) { + channelPromise.setFailure(new UnsupportedOperationException()); + return; + } + + if (connectPromise != null) { + channelPromise.setFailure(new ConnectionPendingException()); + return; + } + + if (remote instanceof QuicConnectionAddress) { + if (!sourceConnectionIds.isEmpty()) { + // If a key is assigned we know this channel was already connected. + channelPromise.setFailure(new AlreadyConnectedException()); + return; + } + + QuicConnectionAddress address = (QuicConnectionAddress) remote; + connectPromise = channelPromise; + connectAddress = address; + + // Schedule connect timeout. + int connectTimeoutMillis = config().getConnectTimeoutMillis(); + if (connectTimeoutMillis > 0) { + connectTimeoutFuture = eventLoop().schedule(() -> { + ChannelPromise connectPromise = QuicheQuicChannel.this.connectPromise; + if (connectPromise != null && !connectPromise.isDone() + && connectPromise.tryFailure(new ConnectTimeoutException( + "connection timed out: " + remote))) { + close(voidPromise()); + } + }, connectTimeoutMillis, TimeUnit.MILLISECONDS); + } + + connectPromise.addListener((ChannelFuture future) -> { + if (future.isCancelled()) { + if (connectTimeoutFuture != null) { + connectTimeoutFuture.cancel(false); + } + connectPromise = null; + close(voidPromise()); + } + }); + + parent().connect(new QuicheQuicChannelAddress(QuicheQuicChannel.this)); + return; + } + + channelPromise.setFailure(new UnsupportedOperationException()); + } + + private void fireConnectCloseEventIfNeeded(long connAddr) { + if (connectionCloseEvent == null) { + connectionCloseEvent = Quiche.quiche_conn_peer_error(connAddr); + if (connectionCloseEvent != null) { + pipeline().fireUserEventTriggered(connectionCloseEvent); + } + } + } + + void connectionRecv(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) { + if (isConnDestroyed()) { + return; + } + int bufferReadable = buffer.readableBytes(); + if (bufferReadable == 0) { + // Nothing to do here. Just return... + // See also https://github.com/cloudflare/quiche/issues/817 + return; + } + + reantranceGuard |= IN_RECV; + boolean close = false; + try { + ByteBuf tmpBuffer = null; + // We need to make a copy if the buffer is read only as recv(...) may modify the input buffer as well. + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.recv + if (buffer.isReadOnly()) { + tmpBuffer = alloc().directBuffer(buffer.readableBytes()); + tmpBuffer.writeBytes(buffer); + buffer = tmpBuffer; + } + long memoryAddress = Quiche.readerMemoryAddress(buffer); + + ByteBuffer recvInfo = connection.nextRecvInfo(); + QuicheRecvInfo.setRecvInfo(recvInfo, sender, recipient); + + SocketAddress oldRemote = remote; + + if (connection.isRecvInfoChanged()) { + // Update the cached address + remote = sender; + } + local = recipient; + + long connAddr = connection.address(); + try { + do { + // Call quiche_conn_recv(...) until we consumed all bytes or we did receive some error. + int res = Quiche.quiche_conn_recv(connAddr, memoryAddress, bufferReadable, + Quiche.memoryAddressWithPosition(recvInfo)); + boolean done; + try { + done = Quiche.throwIfError(res); + } catch (Exception e) { + done = true; + close = Quiche.shouldClose(res); + if (tryFailConnectPromise(e)) { + break; + } + fireExceptionEvents(e); + } + + // Process / schedule all tasks that were created. + Runnable task = connection.sslTask(); + if (task != null) { + if (runTasksDirectly()) { + // Consume all tasks + do { + task.run(); + } while ((task = connection.sslTask()) != null); + processReceived(connAddr); + } else { + runAllTaskRecv(task); + } + } else { + processReceived(connAddr); + } + + if (done) { + break; + } + memoryAddress += res; + bufferReadable -= res; + } while (bufferReadable > 0); + } finally { + buffer.skipBytes((int) (memoryAddress - Quiche.readerMemoryAddress(buffer))); + if (tmpBuffer != null) { + tmpBuffer.release(); + } + } + if (close) { + // Let's close now as there is no way to recover + unsafe().close(newPromise()); + } + } finally { + reantranceGuard &= ~IN_RECV; + } + } + + private void processReceived(long connAddr) { + // Handle pending channelActive if needed. + if (handlePendingChannelActive()) { + // Connection was closed right away. + return; + } + + notifyAboutHandshakeCompletionIfNeeded(null); + + fireConnectCloseEventIfNeeded(connAddr); + + if (Quiche.quiche_conn_is_established(connAddr) || + Quiche.quiche_conn_is_in_early_data(connAddr)) { + long uniLeftOld = uniStreamsLeft; + long bidiLeftOld = bidiStreamsLeft; + // Only fetch new stream info when we used all our credits + if (uniLeftOld == 0 || bidiLeftOld == 0) { + long uniLeft = Quiche.quiche_conn_peer_streams_left_uni(connAddr); + long bidiLeft = Quiche.quiche_conn_peer_streams_left_bidi(connAddr); + uniStreamsLeft = uniLeft; + bidiStreamsLeft = bidiLeft; + if (uniLeftOld != uniLeft || bidiLeftOld != bidiLeft) { + pipeline().fireUserEventTriggered(QuicStreamLimitChangedEvent.INSTANCE); + } + } + + handlePathEvents(connAddr); + + if (handleWritableStreams()) { + // Some data was produced, let's flush. + flushParent(); + } + + datagramReadable = true; + streamReadable = true; + + recvDatagram(); + recvStream(); + } + } + + private void handlePathEvents(long addr) { + long event; + while ((event = Quiche.quiche_conn_path_event_next(addr)) > 0) { + try { + int type = Quiche.quiche_path_event_type(event); + + if (type == Quiche.QUICHE_PATH_EVENT_NEW) { + Object[] ret = Quiche.quiche_path_event_new(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.New(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_VALIDATED) { + Object[] ret = Quiche.quiche_path_event_validated(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.Validated(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_FAILED_VALIDATION) { + Object[] ret = Quiche.quiche_path_event_failed_validation(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.FailedValidation(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_CLOSED) { + Object[] ret = Quiche.quiche_path_event_closed(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.Closed(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_REUSED_SOURCE_CONNECTION_ID) { + Object[] ret = Quiche.quiche_path_event_reused_source_connection_id(event); + Long seq = (Long) ret[0]; + InetSocketAddress localOld = (InetSocketAddress) ret[1]; + InetSocketAddress peerOld = (InetSocketAddress) ret[2]; + InetSocketAddress local = (InetSocketAddress) ret[3]; + InetSocketAddress peer = (InetSocketAddress) ret[4]; + pipeline().fireUserEventTriggered( + new QuicPathEvent.ReusedSourceConnectionId(seq, localOld, peerOld, local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_PEER_MIGRATED) { + Object[] ret = Quiche.quiche_path_event_peer_migrated(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.PeerMigrated(local, peer)); + } + } finally { + Quiche.quiche_path_event_free(event); + } + } + } + + private void runAllTaskRecv(Runnable task) { + sslTaskExecutor.execute(decorateTaskRecv(task)); + } + + private Runnable decorateTaskRecv(Runnable task) { + return () -> { + try { + runAll(task); + } finally { + // Move back to the EventLoop. + eventLoop().execute(() -> { + if (connection != null) { + processReceived(connection.address()); + + // Call connection send to continue handshake if needed. + if (connectionSend()) { + forceFlushParent(); + } + } + }); + } + }; + } + void recv() { + if ((reantranceGuard & IN_RECV) != 0 || isConnDestroyed()) { + return; + } + + long connAddr = connection.address(); + // Check if we can read anything yet. + if (!Quiche.quiche_conn_is_established(connAddr) && + !Quiche.quiche_conn_is_in_early_data(connAddr)) { + return; + } + + reantranceGuard |= IN_RECV; + try { + recvDatagram(); + recvStream(); + } finally { + fireChannelReadCompleteIfNeeded(); + reantranceGuard &= ~IN_RECV; + } + } + + private void recvStream() { + long connAddr = connection.address(); + long readableIterator = Quiche.quiche_conn_readable(connAddr); + if (readableIterator != -1) { + try { + // For streams we always process all streams when at least on read was requested. + if (recvStreamPending && streamReadable) { + for (;;) { + int readable = Quiche.quiche_stream_iter_next( + readableIterator, readableStreams); + for (int i = 0; i < readable; i++) { + long streamId = readableStreams[i]; + QuicheQuicStreamChannel streamChannel = streams.get(streamId); + if (streamChannel == null) { + recvStreamPending = false; + fireChannelReadCompletePending = true; + streamChannel = addNewStreamChannel(streamId); + streamChannel.readable(); + pipeline().fireChannelRead(streamChannel); + } else { + streamChannel.readable(); + } + } + if (readable < readableStreams.length) { + // We did consume all readable streams. + streamReadable = false; + break; + } + } + } + } finally { + Quiche.quiche_stream_iter_free(readableIterator); + } + } + } + + private void recvDatagram() { + if (!supportsDatagram) { + return; + } + long connAddr = connection.address(); + while (recvDatagramPending && datagramReadable) { + @SuppressWarnings("deprecation") + RecvByteBufAllocator.Handle recvHandle = recvBufAllocHandle(); + recvHandle.reset(config()); + + int numMessagesRead = 0; + do { + int len = Quiche.quiche_conn_dgram_recv_front_len(connAddr); + if (len == Quiche.QUICHE_ERR_DONE) { + datagramReadable = false; + return; + } + + ByteBuf datagramBuffer = alloc().directBuffer(len); + recvHandle.attemptedBytesRead(datagramBuffer.writableBytes()); + int writerIndex = datagramBuffer.writerIndex(); + long memoryAddress = Quiche.writerMemoryAddress(datagramBuffer); + + int written = Quiche.quiche_conn_dgram_recv(connAddr, + memoryAddress, datagramBuffer.writableBytes()); + try { + if (Quiche.throwIfError(written)) { + datagramBuffer.release(); + // We did consume all datagram packets. + datagramReadable = false; + break; + } + } catch (Exception e) { + datagramBuffer.release(); + pipeline().fireExceptionCaught(e); + } + recvHandle.lastBytesRead(written); + recvHandle.incMessagesRead(1); + numMessagesRead++; + datagramBuffer.writerIndex(writerIndex + written); + recvDatagramPending = false; + fireChannelReadCompletePending = true; + + pipeline().fireChannelRead(datagramBuffer); + } while (recvHandle.continueReading()); + recvHandle.readComplete(); + + // Check if we produced any messages. + if (numMessagesRead > 0) { + fireChannelReadCompleteIfNeeded(); + } + } + } + + private boolean handlePendingChannelActive() { + long connAddr = connection.address(); + if (server) { + if (state == OPEN && Quiche.quiche_conn_is_established(connAddr)) { + // We didn't notify before about channelActive... Update state and fire the event. + state = ACTIVE; + + pipeline().fireChannelActive(); + notifyAboutHandshakeCompletionIfNeeded(null); + fireDatagramExtensionEvent(); + } + } else if (connectPromise != null && Quiche.quiche_conn_is_established(connAddr)) { + ChannelPromise promise = connectPromise; + connectPromise = null; + state = ACTIVE; + + boolean promiseSet = promise.trySuccess(); + pipeline().fireChannelActive(); + notifyAboutHandshakeCompletionIfNeeded(null); + fireDatagramExtensionEvent(); + if (!promiseSet) { + fireConnectCloseEventIfNeeded(connAddr); + this.close(this.voidPromise()); + return true; + } + } + return false; + } + + private void fireDatagramExtensionEvent() { + long connAddr = connection.address(); + int len = Quiche.quiche_conn_dgram_max_writable_len(connAddr); + // QUICHE_ERR_DONE means the remote peer does not support the extension. + if (len != Quiche.QUICHE_ERR_DONE) { + pipeline().fireUserEventTriggered(new QuicDatagramExtensionEvent(len)); + } + } + + private QuicheQuicStreamChannel addNewStreamChannel(long streamId) { + QuicheQuicStreamChannel streamChannel = new QuicheQuicStreamChannel( + QuicheQuicChannel.this, streamId); + QuicheQuicStreamChannel old = streams.put(streamId, streamChannel); + assert old == null; + streamChannel.writable(streamCapacity(streamId)); + return streamChannel; + } + } + + /** + * Finish the connect of a client channel. + */ + void finishConnect() { + assert !server; + if (connectionSend()) { + flushParent(); + } + } + + private void notifyEarlyDataReadyIfNeeded() { + if (!server && !earlyDataReadyNotified && + !isConnDestroyed() && Quiche.quiche_conn_is_in_early_data(connection.address())) { + earlyDataReadyNotified = true; + pipeline().fireUserEventTriggered(SslEarlyDataReadyEvent.INSTANCE); + } + } + + // TODO: Come up with something better. + static QuicheQuicChannel handleConnect(Function sslEngineProvider, + Executor sslTaskExecutor, + SocketAddress address, long config, int localConnIdLength, + boolean supportsDatagram, ByteBuffer fromSockaddrMemory, + ByteBuffer toSockaddrMemory) throws Exception { + if (address instanceof QuicheQuicChannel.QuicheQuicChannelAddress) { + QuicheQuicChannel.QuicheQuicChannelAddress addr = (QuicheQuicChannel.QuicheQuicChannelAddress) address; + QuicheQuicChannel channel = addr.channel; + channel.connect(sslEngineProvider, sslTaskExecutor, config, localConnIdLength, supportsDatagram, + fromSockaddrMemory, toSockaddrMemory); + return channel; + } + return null; + } + + /** + * Just a container to pass the {@link QuicheQuicChannel} to {@link QuicheQuicClientCodec}. + */ + private static final class QuicheQuicChannelAddress extends SocketAddress { + + final QuicheQuicChannel channel; + + QuicheQuicChannelAddress(QuicheQuicChannel channel) { + this.channel = channel; + } + } + + private final class TimeoutHandler implements Runnable { + private ScheduledFuture timeoutFuture; + private final Consumer timeoutTask; + + TimeoutHandler(Consumer timeoutTask) { + this.timeoutTask = timeoutTask; + } + + @Override + public void run() { + if (!isConnDestroyed()) { + long connAddr = connection.address(); + timeoutFuture = null; + // Notify quiche there was a timeout. + Quiche.quiche_conn_on_timeout(connAddr); + + if (Quiche.quiche_conn_is_closed(connAddr)) { + forceClose(); + if (timeoutTask != null){ + timeoutTask.accept(QuicheQuicChannel.this); + } + } else { + // We need to call connectionSend when a timeout was triggered. + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send. + boolean send = connectionSend(); + if (send) { + flushParent(); + } + if (!closeAllIfConnectionClosed()) { + // The connection is alive, reschedule. + scheduleTimeout(); + } + } + } + } + + // Schedule timeout. + // See https://docs.rs/quiche/0.6.0/quiche/#generating-outgoing-packets + void scheduleTimeout() { + if (isConnDestroyed()) { + cancel(); + return; + } + long nanos = Quiche.quiche_conn_timeout_as_nanos(connection.address()); + if (timeoutFuture == null) { + timeoutFuture = eventLoop().schedule(this, + nanos, TimeUnit.NANOSECONDS); + } else { + long remaining = timeoutFuture.getDelay(TimeUnit.NANOSECONDS); + if (remaining <= 0) { + // This means the timer already elapsed. In this case just cancel the future and call run() + // directly. This will ensure we correctly call quiche_conn_on_timeout() etc. + cancel(); + run(); + } else if (remaining > nanos) { + // The new timeout is smaller then what was scheduled before. Let's cancel the old timeout + // and schedule a new one. + cancel(); + timeoutFuture = eventLoop().schedule(this, nanos, TimeUnit.NANOSECONDS); + } + } + } + + void cancel() { + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + timeoutFuture = null; + } + } + } + + @Override + public Future collectStats(Promise promise) { + if (eventLoop().inEventLoop()) { + collectStats0(promise); + } else { + eventLoop().execute(() -> collectStats0(promise)); + } + return promise; + } + + private void collectStats0(Promise promise) { + if (isConnDestroyed()) { + promise.setSuccess(statsAtClose); + return; + } + + collectStats0(connection, promise); + } + + private QuicConnectionStats collectStats0(QuicheQuicConnection connection, Promise promise) { + final long[] stats = Quiche.quiche_conn_stats(connection.address()); + if (stats == null) { + promise.setFailure(new IllegalStateException("native quiche_conn_stats(...) failed")); + return null; + } + + final QuicheQuicConnectionStats connStats = + new QuicheQuicConnectionStats(stats); + promise.setSuccess(connStats); + return connStats; + } + + @Override + public QuicTransportParameters peerTransportParameters() { + return connection.peerParameters(); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannelConfig.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannelConfig.java new file mode 100644 index 0000000..67988c1 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannelConfig.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; + +import java.util.Map; + +/** + * Default {@link QuicChannelConfig} implementation. + */ +final class QuicheQuicChannelConfig extends DefaultChannelConfig implements QuicChannelConfig { + + private volatile QLogConfiguration qLogConfiguration; + private volatile SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator = + SegmentedDatagramPacketAllocator.NONE; + + QuicheQuicChannelConfig(Channel channel) { + super(channel); + } + + @Override + public Map, Object> getOptions() { + return getOptions(super.getOptions(), + QuicChannelOption.QLOG, QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == QuicChannelOption.QLOG) { + return (T) getQLogConfiguration(); + } + if (option == QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR) { + return (T) getSegmentedDatagramPacketAllocator(); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + if (option == QuicChannelOption.QLOG) { + setQLogConfiguration((QLogConfiguration) value); + return true; + } + if (option == QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR) { + setSegmentedDatagramPacketAllocator((SegmentedDatagramPacketAllocator) value); + return true; + } + return super.setOption(option, value); + } + + @Override + public QuicChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + @Deprecated + public QuicChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public QuicChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public QuicChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public QuicChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public QuicChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public QuicChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + public QuicChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + public QuicChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public QuicChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public QuicChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + QLogConfiguration getQLogConfiguration() { + return qLogConfiguration; + } + + private void setQLogConfiguration(QLogConfiguration qLogConfiguration) { + if (channel.isRegistered()) { + throw new IllegalStateException("QLOG can only be enabled before the Channel was registered"); + } + this.qLogConfiguration = qLogConfiguration; + } + + SegmentedDatagramPacketAllocator getSegmentedDatagramPacketAllocator() { + return segmentedDatagramPacketAllocator; + } + + private void setSegmentedDatagramPacketAllocator( + SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator) { + this.segmentedDatagramPacketAllocator = segmentedDatagramPacketAllocator; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicClientCodec.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicClientCodec.java new file mode 100644 index 0000000..939266c --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicClientCodec.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * {@link QuicheQuicCodec} for QUIC clients. + */ +final class QuicheQuicClientCodec extends QuicheQuicCodec { + + private final Function sslEngineProvider; + private final Executor sslTaskExecutor; + + QuicheQuicClientCodec(QuicheConfig config, Function sslEngineProvider, + Executor sslTaskExecutor, int localConnIdLength, FlushStrategy flushStrategy) { + // Let's just use Quic.MAX_DATAGRAM_SIZE as the maximum size for a token on the client side. This should be + // safe enough and as we not have too many codecs at the same time this should be ok. + super(config, localConnIdLength, Quic.MAX_DATAGRAM_SIZE, flushStrategy); + this.sslEngineProvider = sslEngineProvider; + this.sslTaskExecutor = sslTaskExecutor; + } + + @Override + protected QuicheQuicChannel quicPacketRead( + ChannelHandlerContext ctx, InetSocketAddress sender, InetSocketAddress recipient, + QuicPacketType type, int version, ByteBuf scid, ByteBuf dcid, + ByteBuf token) { + ByteBuffer key = dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()); + return getChannel(key); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + final QuicheQuicChannel channel; + try { + channel = QuicheQuicChannel.handleConnect(sslEngineProvider, sslTaskExecutor, remoteAddress, config.nativeAddress(), + localConnIdLength, config.isDatagramSupported(), + senderSockaddrMemory.internalNioBuffer(0, senderSockaddrMemory.capacity()), + recipientSockaddrMemory.internalNioBuffer(0, recipientSockaddrMemory.capacity())); + } catch (Exception e) { + promise.setFailure(e); + return; + } + if (channel != null) { + addChannel(channel); + + channel.finishConnect(); + promise.setSuccess(); + return; + } + ctx.connect(remoteAddress, localAddress, promise); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicCodec.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicCodec.java new file mode 100644 index 0000000..478f9f3 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicCodec.java @@ -0,0 +1,315 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +import static io.netty.handler.codec.quic.Quiche.allocateNativeOrder; + +/** + * Abstract base class for QUIC codecs. + */ +abstract class QuicheQuicCodec extends ChannelDuplexHandler { + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicheQuicCodec.class); + + private final Map connectionIdToChannel = new HashMap<>(); + private final Set channels = new HashSet<>(); + private final Queue needsFireChannelReadComplete = new ArrayDeque<>(); + private final int maxTokenLength; + private final FlushStrategy flushStrategy; + + private MessageSizeEstimator.Handle estimatorHandle; + private QuicHeaderParser headerParser; + private QuicHeaderParser.QuicHeaderProcessor parserCallback; + private int pendingBytes; + private int pendingPackets; + private boolean inChannelReadComplete; + + protected final QuicheConfig config; + protected final int localConnIdLength; + // This buffer is used to copy InetSocketAddress to sockaddr_storage and so pass it down the JNI layer. + protected ByteBuf senderSockaddrMemory; + protected ByteBuf recipientSockaddrMemory; + + QuicheQuicCodec(QuicheConfig config, int localConnIdLength, int maxTokenLength, FlushStrategy flushStrategy) { + this.config = config; + this.localConnIdLength = localConnIdLength; + this.maxTokenLength = maxTokenLength; + this.flushStrategy = flushStrategy; + } + + protected final QuicheQuicChannel getChannel(ByteBuffer key) { + return connectionIdToChannel.get(key); + } + + protected final void addMapping(ByteBuffer key, QuicheQuicChannel channel) { + connectionIdToChannel.put(key, channel); + } + + protected final void removeMapping(ByteBuffer key) { + connectionIdToChannel.remove(key); + } + + protected final void removeChannel(QuicheQuicChannel channel) { + boolean removed = channels.remove(channel); + assert removed; + for (ByteBuffer id : channel.sourceConnectionIds()) { + connectionIdToChannel.remove(id); + } + } + + protected final void addChannel(QuicheQuicChannel channel) { + boolean added = channels.add(channel); + assert added; + for (ByteBuffer id : channel.sourceConnectionIds()) { + connectionIdToChannel.put(id, channel); + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + senderSockaddrMemory = allocateNativeOrder(Quiche.SIZEOF_SOCKADDR_STORAGE); + recipientSockaddrMemory = allocateNativeOrder(Quiche.SIZEOF_SOCKADDR_STORAGE); + headerParser = new QuicHeaderParser(maxTokenLength, localConnIdLength); + parserCallback = new QuicCodecHeaderProcessor(ctx); + estimatorHandle = ctx.channel().config().getMessageSizeEstimator().newHandle(); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + try { + // Use a copy of the array as closing the channel may cause an unwritable event that could also + // remove channels. + for (QuicheQuicChannel ch : channels.toArray(new QuicheQuicChannel[0])) { + ch.forceClose(); + } + channels.clear(); + connectionIdToChannel.clear(); + + needsFireChannelReadComplete.clear(); + if (pendingPackets > 0) { + flushNow(ctx); + } + } finally { + config.free(); + if (senderSockaddrMemory != null) { + senderSockaddrMemory.release(); + } + if (recipientSockaddrMemory != null) { + recipientSockaddrMemory.release(); + } + if (headerParser != null) { + headerParser.close(); + headerParser = null; + } + } + } + + @Override + public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + DatagramPacket packet = (DatagramPacket) msg; + try { + ByteBuf buffer = ((DatagramPacket) msg).content(); + if (!buffer.isDirect()) { + // We need a direct buffer as otherwise we can not access the memoryAddress. + // Let's do a copy to direct memory. + ByteBuf direct = ctx.alloc().directBuffer(buffer.readableBytes()); + try { + direct.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes()); + handleQuicPacket(packet.sender(), packet.recipient(), direct); + } finally { + direct.release(); + } + } else { + handleQuicPacket(packet.sender(), packet.recipient(), buffer); + } + } finally { + packet.release(); + } + } + + private void handleQuicPacket(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) { + try { + headerParser.parse(sender, recipient, buffer, parserCallback); + } catch (Exception e) { + LOGGER.debug("Error while processing QUIC packet", e); + } + } + + /** + * Handle a QUIC packet and return {@code true} if we need to call {@link ChannelHandlerContext#flush()}. + * + * @param ctx the {@link ChannelHandlerContext}. + * @param sender the {@link InetSocketAddress} of the sender of the QUIC packet + * @param recipient the {@link InetSocketAddress} of the recipient of the QUIC packet + * @param type the type of the packet. + * @param version the QUIC version + * @param scid the source connection id. + * @param dcid the destination connection id + * @param token the token + * @return {@code true} if we need to call {@link ChannelHandlerContext#flush()} before there is no new events + * for this handler in the current eventloop run. + * @throws Exception thrown if there is an error during processing. + */ + protected abstract QuicheQuicChannel quicPacketRead(ChannelHandlerContext ctx, InetSocketAddress sender, + InetSocketAddress recipient, QuicPacketType type, int version, + ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception; + + @Override + public final void channelReadComplete(ChannelHandlerContext ctx) { + inChannelReadComplete = true; + try { + for (;;) { + QuicheQuicChannel channel = needsFireChannelReadComplete.poll(); + if (channel == null) { + break; + } + channel.recvComplete(); + if (channel.freeIfClosed()) { + removeChannel(channel); + } + } + } finally { + inChannelReadComplete = false; + if (pendingPackets > 0) { + flushNow(ctx); + } + } + } + + @Override + public final void channelWritabilityChanged(ChannelHandlerContext ctx) { + if (ctx.channel().isWritable()) { + List closed = null; + for (QuicheQuicChannel channel : channels) { + // TODO: Be a bit smarter about this. + channel.writable(); + if (channel.freeIfClosed()) { + if (closed == null) { + closed = new ArrayList<>(); + } + closed.add(channel); + } + } + if (closed != null) { + for (QuicheQuicChannel ch: closed) { + removeChannel(ch); + } + } + } else { + // As we batch flushes we need to ensure we at least try to flush a batch once the channel becomes + // unwritable. Otherwise we may end up with buffering too much writes and so waste memory. + ctx.flush(); + } + + ctx.fireChannelWritabilityChanged(); + } + + @Override + public final void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + pendingPackets ++; + int size = estimatorHandle.size(msg); + if (size > 0) { + pendingBytes += size; + } + try { + ctx.write(msg, promise); + } finally { + flushIfNeeded(ctx); + } + } + + @Override + public final void flush(ChannelHandlerContext ctx) { + // If we are in the channelReadComplete(...) method we might be able to delay the flush(...) until we finish + // processing all channels. + if (inChannelReadComplete) { + flushIfNeeded(ctx); + } else if (pendingPackets > 0) { + flushNow(ctx); + } + } + + private void flushIfNeeded(ChannelHandlerContext ctx) { + // Check if we should force a flush() and so ensure the packets are delivered in a timely + // manner and also make room in the outboundbuffer again that belongs to the underlying channel. + if (flushStrategy.shouldFlushNow(pendingPackets, pendingBytes)) { + flushNow(ctx); + } + } + + private void flushNow(ChannelHandlerContext ctx) { + pendingBytes = 0; + pendingPackets = 0; + ctx.flush(); + } + + private final class QuicCodecHeaderProcessor implements QuicHeaderParser.QuicHeaderProcessor { + + private final ChannelHandlerContext ctx; + + QuicCodecHeaderProcessor(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + @Override + public void process(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer, QuicPacketType type, + int version, ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception { + QuicheQuicChannel channel = quicPacketRead(ctx, sender, recipient, + type, version, scid, + dcid, token); + if (channel != null) { + channelRecv(channel, sender, recipient, buffer); + } + } + } + + /** + * Called once something was received for a {@link QuicheQuicChannel}. + * + * @param channel the channel for which the data was received + * @param sender the sender + * @param recipient the recipient + * @param buffer the acutal data. + */ + protected void channelRecv(QuicheQuicChannel channel, InetSocketAddress sender, + InetSocketAddress recipient, ByteBuf buffer) { + // Add to queue first, we might be able to safe some flushes and consolidate them + // in channelReadComplete(...) this way. + if (channel.markInFireChannelReadCompleteQueue()) { + needsFireChannelReadComplete.add(channel); + } + channel.recv(sender, recipient, buffer); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnection.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnection.java new file mode 100644 index 0000000..c561c7c --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnection.java @@ -0,0 +1,232 @@ +/* + * 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.ResourceLeakTracker; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.function.Supplier; + +final class QuicheQuicConnection { + private static final int TOTAL_RECV_INFO_SIZE = Quiche.SIZEOF_QUICHE_RECV_INFO + + Quiche.SIZEOF_SOCKADDR_STORAGE + Quiche.SIZEOF_SOCKADDR_STORAGE; + private static final ResourceLeakDetector leakDetector = + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(QuicheQuicConnection.class); + private final QuicheQuicSslEngine engine; + + private final ResourceLeakTracker leakTracker; + + final long ssl; + private ReferenceCounted refCnt; + + // This block of memory is used to store the following structs (in this order): + // - quiche_recv_info + // - sockaddr_storage + // - quiche_recv_info + // - sockaddr_storage + // - quiche_send_info + // - quiche_send_info + // + // We need to have every stored 2 times as we need to check if the last sockaddr has changed between + // quiche_conn_recv and quiche_conn_send calls. If this happens we know a QUIC connection migration did happen. + private final ByteBuf recvInfoBuffer; + private final ByteBuf sendInfoBuffer; + + private boolean recvInfoFirst = true; + private boolean sendInfoFirst = true; + private final ByteBuffer recvInfoBuffer1; + private final ByteBuffer recvInfoBuffer2; + private final ByteBuffer sendInfoBuffer1; + private final ByteBuffer sendInfoBuffer2; + + private long connection; + + QuicheQuicConnection(long connection, long ssl, QuicheQuicSslEngine engine, ReferenceCounted refCnt) { + this.connection = connection; + this.ssl = ssl; + this.engine = engine; + this.refCnt = refCnt; + // TODO: Maybe cache these per thread as we only use them temporary within a limited scope. + recvInfoBuffer = Quiche.allocateNativeOrder(2 * TOTAL_RECV_INFO_SIZE); + sendInfoBuffer = Quiche.allocateNativeOrder(2 * Quiche.SIZEOF_QUICHE_SEND_INFO); + + // Let's memset the memory. + recvInfoBuffer.setZero(0, recvInfoBuffer.capacity()); + sendInfoBuffer.setZero(0, sendInfoBuffer.capacity()); + + recvInfoBuffer1 = recvInfoBuffer.nioBuffer(0, TOTAL_RECV_INFO_SIZE); + recvInfoBuffer2 = recvInfoBuffer.nioBuffer(TOTAL_RECV_INFO_SIZE, TOTAL_RECV_INFO_SIZE); + + sendInfoBuffer1 = sendInfoBuffer.nioBuffer(0, Quiche.SIZEOF_QUICHE_SEND_INFO); + sendInfoBuffer2 = sendInfoBuffer.nioBuffer(Quiche.SIZEOF_QUICHE_SEND_INFO, Quiche.SIZEOF_QUICHE_SEND_INFO); + this.engine.connection = this; + leakTracker = leakDetector.track(this); + } + + synchronized void reattach(ReferenceCounted refCnt) { + this.refCnt.release(); + this.refCnt = refCnt; + } + + void free() { + free(true); + } + + private void free(boolean closeLeakTracker) { + boolean release = false; + synchronized (this) { + if (connection != -1) { + try { + BoringSSL.SSL_cleanup(ssl); + Quiche.quiche_conn_free(connection); + engine.ctx.remove(engine); + release = true; + refCnt.release(); + } finally { + connection = -1; + } + } + } + if (release) { + recvInfoBuffer.release(); + sendInfoBuffer.release(); + if (closeLeakTracker && leakTracker != null) { + leakTracker.close(this); + } + } + } + + Runnable sslTask() { + final Runnable task; + synchronized (this) { + if (connection != -1) { + task = BoringSSL.SSL_getTask(ssl); + } else { + task = null; + } + } + if (task == null) { + return null; + } + + return () -> { + if (connection == -1) { + return; + } + + task.run(); + }; + } + + QuicConnectionAddress sourceId() { + return connectionId(() -> Quiche.quiche_conn_source_id(connection)); + } + + QuicConnectionAddress destinationId() { + return connectionId(() -> Quiche.quiche_conn_destination_id(connection)); + } + + QuicConnectionAddress connectionId(Supplier idSupplier) { + final byte[] id; + synchronized (this) { + if (connection == -1) { + return null; + } + id = idSupplier.get(); + } + return id == null ? null : new QuicConnectionAddress(id); + } + + QuicheQuicTransportParameters peerParameters() { + final long[] ret; + synchronized (this) { + if (connection == -1) { + return null; + } + ret = Quiche.quiche_conn_peer_transport_params(connection); + } + if (ret == null) { + return null; + } + return new QuicheQuicTransportParameters(ret); + } + + QuicheQuicSslEngine engine() { + return engine; + } + + long address() { + assert connection != -1; + return connection; + } + + void initInfo(InetSocketAddress local, InetSocketAddress remote) { + assert connection != -1; + assert recvInfoBuffer.refCnt() != 0; + assert sendInfoBuffer.refCnt() != 0; + + // Fill both quiche_recv_info structs with the same address. + QuicheRecvInfo.setRecvInfo(recvInfoBuffer1, remote, local); + QuicheRecvInfo.setRecvInfo(recvInfoBuffer2, remote, local); + + // Fill both quiche_send_info structs with the same address. + QuicheSendInfo.setSendInfo(sendInfoBuffer1, local, remote); + QuicheSendInfo.setSendInfo(sendInfoBuffer2, local, remote); + } + + ByteBuffer nextRecvInfo() { + assert recvInfoBuffer.refCnt() != 0; + recvInfoFirst = !recvInfoFirst; + return recvInfoFirst ? recvInfoBuffer1 : recvInfoBuffer2; + } + + ByteBuffer nextSendInfo() { + assert sendInfoBuffer.refCnt() != 0; + sendInfoFirst = !sendInfoFirst; + return sendInfoFirst ? sendInfoBuffer1 : sendInfoBuffer2; + } + + boolean isSendInfoChanged() { + assert sendInfoBuffer.refCnt() != 0; + return !QuicheSendInfo.isSameAddress(sendInfoBuffer1, sendInfoBuffer2); + } + + boolean isRecvInfoChanged() { + assert recvInfoBuffer.refCnt() != 0; + return !QuicheRecvInfo.isSameAddress(recvInfoBuffer1, recvInfoBuffer2); + } + + boolean isClosed() { + assert connection != -1; + return Quiche.quiche_conn_is_closed(connection); + } + + // Let's override finalize() as we want to ensure we never leak memory even if the user will miss to close + // Channel that uses this connection and just let it get GC'ed + @Override + protected void finalize() throws Throwable { + try { + free(false); + } finally { + super.finalize(); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnectionStats.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnectionStats.java new file mode 100644 index 0000000..87cf1ab --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnectionStats.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.util.internal.StringUtil; + +final class QuicheQuicConnectionStats implements QuicConnectionStats { + + private final long[] values; + + QuicheQuicConnectionStats(long[] values) { + this.values = values; + } + + @Override + public long recv() { + return values[0]; + } + + @Override + public long sent() { + return values[1]; + } + + @Override + public long lost() { + return values[2]; + } + + @Override + public long retrans() { + return values[3]; + } + + @Override + public long sentBytes() { + return values[4]; + } + + @Override + public long recvBytes() { + return values[5]; + } + + @Override + public long lostBytes() { + return values[6]; + } + + @Override + public long streamRetransBytes() { + return values[7]; + } + + @Override + public long pathsCount() { + return values[8]; + } + + /** + * Returns the {@link String} representation of stats. + */ + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "[" + + "recv=" + recv() + + ", sent=" + sent() + + ", lost=" + lost() + + ", retrans=" + retrans() + + ", sentBytes=" + sentBytes() + + ", recvBytes=" + recvBytes() + + ", lostBytes=" + lostBytes() + + ", streamRetransBytes=" + streamRetransBytes() + + ", pathsCount=" + pathsCount() + + "]"; + } + +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicServerCodec.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicServerCodec.java new file mode 100644 index 0000000..edd6e71 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicServerCodec.java @@ -0,0 +1,268 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.AttributeKey; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.Function; + +import static io.netty.handler.codec.quic.Quiche.allocateNativeOrder; + +/** + * {@link QuicheQuicCodec} for QUIC servers. + */ +final class QuicheQuicServerCodec extends QuicheQuicCodec { + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicheQuicServerCodec.class); + + private final Function sslEngineProvider; + private final Executor sslTaskExecutor; + private final QuicConnectionIdGenerator connectionIdAddressGenerator; + private final QuicResetTokenGenerator resetTokenGenerator; + private final QuicTokenHandler tokenHandler; + private final ChannelHandler handler; + private final Map.Entry, Object>[] optionsArray; + private final Map.Entry, Object>[] attrsArray; + private final ChannelHandler streamHandler; + private final Map.Entry, Object>[] streamOptionsArray; + private final Map.Entry, Object>[] streamAttrsArray; + private ByteBuf mintTokenBuffer; + private ByteBuf connIdBuffer; + + QuicheQuicServerCodec(QuicheConfig config, + int localConnIdLength, + QuicTokenHandler tokenHandler, + QuicConnectionIdGenerator connectionIdAddressGenerator, + QuicResetTokenGenerator resetTokenGenerator, + FlushStrategy flushStrategy, + Function sslEngineProvider, + Executor sslTaskExecutor, + ChannelHandler handler, + Map.Entry, Object>[] optionsArray, + Map.Entry, Object>[] attrsArray, + ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray) { + super(config, localConnIdLength, tokenHandler.maxTokenLength(), flushStrategy); + this.tokenHandler = tokenHandler; + this.connectionIdAddressGenerator = connectionIdAddressGenerator; + this.resetTokenGenerator = resetTokenGenerator; + this.sslEngineProvider = sslEngineProvider; + this.sslTaskExecutor = sslTaskExecutor; + this.handler = handler; + this.optionsArray = optionsArray; + this.attrsArray = attrsArray; + this.streamHandler = streamHandler; + this.streamOptionsArray = streamOptionsArray; + this.streamAttrsArray = streamAttrsArray; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + super.handlerAdded(ctx); + connIdBuffer = Quiche.allocateNativeOrder(localConnIdLength); + mintTokenBuffer = allocateNativeOrder(tokenHandler.maxTokenLength()); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + super.handlerRemoved(ctx); + if (connIdBuffer != null) { + connIdBuffer.release(); + } + if (mintTokenBuffer != null) { + mintTokenBuffer.release(); + } + } + + @Override + protected void channelRecv(QuicheQuicChannel channel, InetSocketAddress sender, + InetSocketAddress recipient, ByteBuf buffer) { + super.channelRecv(channel, sender, recipient, buffer); + for (ByteBuffer retiredSourceConnectionId : channel.retiredSourceConnectionId()) { + removeMapping(retiredSourceConnectionId); + } + for (ByteBuffer newSourceConnectionId : + channel.newSourceConnectionIds(connectionIdAddressGenerator, resetTokenGenerator)) { + addMapping(newSourceConnectionId, channel); + } + } + + @Override + protected QuicheQuicChannel quicPacketRead(ChannelHandlerContext ctx, InetSocketAddress sender, + InetSocketAddress recipient, QuicPacketType type, int version, + ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception { + ByteBuffer dcidByteBuffer = dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()); + QuicheQuicChannel channel = getChannel(dcidByteBuffer); + if (channel == null && type == QuicPacketType.ZERO_RTT && connectionIdAddressGenerator.isIdempotent()) { + // 0 rtt packet should obtain the server generated dcid + channel = getChannel(connectionIdAddressGenerator.newId(dcidByteBuffer, localConnIdLength)); + } + if (channel == null) { + return handleServer(ctx, sender, recipient, type, version, scid, dcid, token); + } + + return channel; + } + + private QuicheQuicChannel handleServer(ChannelHandlerContext ctx, InetSocketAddress sender, + InetSocketAddress recipient, + @SuppressWarnings("unused") QuicPacketType type, int version, + ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception { + if (!Quiche.quiche_version_is_supported(version)) { + // Version is not supported, try to negotiate it. + ByteBuf out = ctx.alloc().directBuffer(Quic.MAX_DATAGRAM_SIZE); + int outWriterIndex = out.writerIndex(); + + int res = Quiche.quiche_negotiate_version( + Quiche.readerMemoryAddress(scid), scid.readableBytes(), + Quiche.readerMemoryAddress(dcid), dcid.readableBytes(), + Quiche.writerMemoryAddress(out), out.writableBytes()); + if (res < 0) { + out.release(); + Quiche.throwIfError(res); + } else { + ctx.writeAndFlush(new DatagramPacket(out.writerIndex(outWriterIndex + res), sender)); + } + return null; + } + + final int offset; + boolean noToken = false; + if (!token.isReadable()) { + // Clear buffers so we can reuse these. + mintTokenBuffer.clear(); + connIdBuffer.clear(); + + // The remote peer did not send a token. + if (tokenHandler.writeToken(mintTokenBuffer, dcid, sender)) { + ByteBuffer connId = connectionIdAddressGenerator.newId( + dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()), localConnIdLength); + connIdBuffer.writeBytes(connId); + + ByteBuf out = ctx.alloc().directBuffer(Quic.MAX_DATAGRAM_SIZE); + int outWriterIndex = out.writerIndex(); + int written = Quiche.quiche_retry( + Quiche.readerMemoryAddress(scid), scid.readableBytes(), + Quiche.readerMemoryAddress(dcid), dcid.readableBytes(), + Quiche.readerMemoryAddress(connIdBuffer), connIdBuffer.readableBytes(), + Quiche.readerMemoryAddress(mintTokenBuffer), mintTokenBuffer.readableBytes(), + version, + Quiche.writerMemoryAddress(out), out.writableBytes()); + + if (written < 0) { + out.release(); + Quiche.throwIfError(written); + } else { + ctx.writeAndFlush(new DatagramPacket(out.writerIndex(outWriterIndex + written), sender)); + } + return null; + } + offset = 0; + noToken = true; + } else { + offset = tokenHandler.validateToken(token, sender); + if (offset == -1) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("invalid token: {}", token.toString(CharsetUtil.US_ASCII)); + } + return null; + } + } + + final ByteBuffer key; + final long scidAddr; + final int scidLen; + final long ocidAddr; + final int ocidLen; + + if (noToken) { + connIdBuffer.clear(); + key = connectionIdAddressGenerator.newId( + dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()), localConnIdLength); + connIdBuffer.writeBytes(key.duplicate()); + scidAddr = Quiche.readerMemoryAddress(connIdBuffer); + scidLen = localConnIdLength; + ocidAddr = -1; + ocidLen = -1; + + QuicheQuicChannel existingChannel = getChannel(key); + if (existingChannel != null) { + return existingChannel; + } + } else { + scidAddr = Quiche.readerMemoryAddress(dcid); + scidLen = localConnIdLength; + ocidAddr = Quiche.memoryAddress(token, offset, token.readableBytes()); + ocidLen = token.readableBytes() - offset; + // Now create the key to store the channel in the map. + byte[] bytes = new byte[localConnIdLength]; + dcid.getBytes(dcid.readerIndex(), bytes); + key = ByteBuffer.wrap(bytes); + } + QuicheQuicChannel channel = QuicheQuicChannel.forServer( + ctx.channel(), key, recipient, sender, config.isDatagramSupported(), + streamHandler, streamOptionsArray, streamAttrsArray, this::removeChannel, sslTaskExecutor); + + Quic.setupChannel(channel, optionsArray, attrsArray, handler, LOGGER); + QuicSslEngine engine = sslEngineProvider.apply(channel); + if (!(engine instanceof QuicheQuicSslEngine)) { + channel.unsafe().closeForcibly(); + throw new IllegalArgumentException("QuicSslEngine is not of type " + + QuicheQuicSslEngine.class.getSimpleName()); + } + if (engine.getUseClientMode()) { + channel.unsafe().closeForcibly(); + throw new IllegalArgumentException("QuicSslEngine is not created in server mode"); + } + + QuicheQuicSslEngine quicSslEngine = (QuicheQuicSslEngine) engine; + QuicheQuicConnection connection = quicSslEngine.createConnection(ssl -> { + ByteBuffer localAddrMemory = recipientSockaddrMemory.internalNioBuffer(0, recipientSockaddrMemory.capacity()); + int localLen = SockaddrIn.setAddress(localAddrMemory, recipient); + + ByteBuffer peerAddrMemory = senderSockaddrMemory.internalNioBuffer(0, senderSockaddrMemory.capacity()); + int peerLen = SockaddrIn.setAddress(peerAddrMemory, sender); + return Quiche.quiche_conn_new_with_tls(scidAddr, scidLen, ocidAddr, ocidLen, + Quiche.memoryAddressWithPosition(localAddrMemory), localLen, + Quiche.memoryAddressWithPosition(peerAddrMemory), peerLen, + config.nativeAddress(), ssl, true); + }); + if (connection == null) { + channel.unsafe().closeForcibly(); + LOGGER.debug("quiche_accept failed"); + return null; + } + + channel.attachQuicheConnection(connection); + + addChannel(channel); + + ctx.channel().eventLoop().register(channel); + return channel; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslContext.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslContext.java new file mode 100644 index 0000000..9b031dd --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslContext.java @@ -0,0 +1,497 @@ +/* + * 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.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.ApplicationProtocolNegotiator; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.AbstractReferenceCounted; +import io.netty.util.Mapping; +import io.netty.util.ReferenceCounted; + +import javax.crypto.NoSuchPaddingException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509TrustManager; +import java.io.File; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.LongFunction; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.util.Objects.requireNonNull; + +final class QuicheQuicSslContext extends QuicSslContext { + final ClientAuth clientAuth; + private final boolean server; + @SuppressWarnings("deprecation") + private final ApplicationProtocolNegotiator apn; + private long sessionCacheSize; + private long sessionTimeout; + private final QuicheQuicSslSessionContext sessionCtx; + private final QuicheQuicSslEngineMap engineMap = new QuicheQuicSslEngineMap(); + private final QuicClientSessionCache sessionCache; + + private final BoringSSLSessionTicketCallback sessionTicketCallback = new BoringSSLSessionTicketCallback(); + + final NativeSslContext nativeSslContext; + + QuicheQuicSslContext(boolean server, long sessionTimeout, long sessionCacheSize, + ClientAuth clientAuth, TrustManagerFactory trustManagerFactory, + KeyManagerFactory keyManagerFactory, String password, + Mapping mapping, + Boolean earlyData, BoringSSLKeylog keylog, + String... applicationProtocols) { + Quic.ensureAvailability(); + this.server = server; + this.clientAuth = server ? checkNotNull(clientAuth, "clientAuth") : ClientAuth.NONE; + final X509TrustManager trustManager; + if (trustManagerFactory == null) { + try { + trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + trustManager = chooseTrustManager(trustManagerFactory); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } else { + trustManager = chooseTrustManager(trustManagerFactory); + } + final X509ExtendedKeyManager keyManager; + if (keyManagerFactory == null) { + if (server) { + throw new IllegalArgumentException("No KeyManagerFactory"); + } + keyManager = null; + } else { + keyManager = chooseKeyManager(keyManagerFactory); + } + final BoringSSLPrivateKeyMethod privateKeyMethod; + if (keyManagerFactory instanceof BoringSSLKeylessManagerFactory) { + privateKeyMethod = new BoringSSLAsyncPrivateKeyMethodAdapter(engineMap, + ((BoringSSLKeylessManagerFactory) keyManagerFactory).privateKeyMethod); + } else { + privateKeyMethod = null; + } + 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 == null ? null : new BoringSSLKeylogCallback(engineMap, keylog), + server ? null : new BoringSSLSessionCallback(engineMap, sessionCache), privateKeyMethod, + sessionTicketCallback, verifyMode, + BoringSSL.subjectNames(trustManager.getAcceptedIssuers()))); + apn = new QuicheQuicApplicationProtocolNegotiator(applicationProtocols); + 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 + BoringSSL.SSLContext_setSessionCacheSize( + nativeSslContext.address(), sessionCacheSize); + this.sessionCacheSize = sessionCacheSize; + + BoringSSL.SSLContext_setSessionCacheTimeout( + nativeSslContext.address(), sessionTimeout); + this.sessionTimeout = sessionTimeout; + } + if (earlyData != null) { + BoringSSL.SSLContext_set_early_data_enabled(nativeSslContext.address(), earlyData); + } + sessionCtx = new QuicheQuicSslSessionContext(this); + } + + private X509ExtendedKeyManager chooseKeyManager(KeyManagerFactory keyManagerFactory) { + for (KeyManager manager: keyManagerFactory.getKeyManagers()) { + if (manager instanceof X509ExtendedKeyManager) { + return (X509ExtendedKeyManager) manager; + } + } + throw new IllegalArgumentException("No X509ExtendedKeyManager included"); + } + + private static X509TrustManager chooseTrustManager(TrustManagerFactory trustManagerFactory) { + for (TrustManager manager: trustManagerFactory.getTrustManagers()) { + if (manager instanceof X509TrustManager) { + return (X509TrustManager) manager; + } + } + throw new IllegalArgumentException("No X509TrustManager included"); + } + + static X509Certificate[] toX509Certificates0(File file) throws CertificateException { + return toX509Certificates(file); + } + + static PrivateKey toPrivateKey0(File keyFile, String keyPassword) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeySpecException, + InvalidAlgorithmParameterException, + KeyException, IOException { + return toPrivateKey(keyFile, keyPassword); + } + + static TrustManagerFactory buildTrustManagerFactory0( + X509Certificate[] certCollection) + throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException { + return buildTrustManagerFactory(certCollection, null, null); + } + + private static int boringSSLVerifyModeForServer(ClientAuth mode) { + switch (mode) { + case NONE: + return BoringSSL.SSL_VERIFY_NONE; + case REQUIRE: + return BoringSSL.SSL_VERIFY_PEER | BoringSSL.SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + case OPTIONAL: + return BoringSSL.SSL_VERIFY_PEER; + default: + throw new Error(mode.toString()); + } + } + + QuicheQuicConnection createConnection(LongFunction connectionCreator, QuicheQuicSslEngine engine) { + nativeSslContext.retain(); + long ssl = BoringSSL.SSL_new(nativeSslContext.address(), isServer(), engine.tlsHostName); + engineMap.put(ssl, engine); + long connection = connectionCreator.apply(ssl); + if (connection == -1) { + engineMap.remove(ssl); + // We retained before but as we don't create a QuicheQuicConnection and transfer ownership we need to + // explict call release again here. + nativeSslContext.release(); + return null; + } + // The connection will call nativeSslContext.release() once it is freed. + return new QuicheQuicConnection(connection, ssl, engine, nativeSslContext); + } + + /** + * Add the given engine to this context + * + * @param engine the engine to add. + * @return the pointer address of this context. + */ + long add(QuicheQuicSslEngine engine) { + nativeSslContext.retain(); + engine.connection.reattach(nativeSslContext); + engineMap.put(engine.connection.ssl, engine); + return nativeSslContext.address(); + } + + /** + * Remove the given engine from this context. + * + * @param engine the engine to remove. + */ + void remove(QuicheQuicSslEngine engine) { + QuicheQuicSslEngine removed = engineMap.remove(engine.connection.ssl); + assert removed == null || removed == engine; + engine.removeSessionFromCacheIfInvalid(); + } + + QuicClientSessionCache getSessionCache() { + return sessionCache; + } + + @Override + public boolean isClient() { + return !server; + } + + @Override + public List cipherSuites() { + return Arrays.asList("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"); + } + + @Override + public long sessionCacheSize() { + if (sessionCache != null) { + return sessionCache.getSessionCacheSize(); + } else { + synchronized (this) { + return sessionCacheSize; + } + } + } + + @Override + public long sessionTimeout() { + if (sessionCache != null) { + return sessionCache.getSessionTimeout(); + } else { + synchronized (this) { + return sessionTimeout; + } + } + } + + @Override + public ApplicationProtocolNegotiator applicationProtocolNegotiator() { + return apn; + } + + @Override + public QuicSslEngine newEngine(ByteBufAllocator alloc) { + return new QuicheQuicSslEngine(this, null, -1); + } + + @Override + public QuicSslEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { + return new QuicheQuicSslEngine(this, peerHost, peerPort); + } + + @Override + public QuicSslSessionContext sessionContext() { + return sessionCtx; + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls) { + throw new UnsupportedOperationException(); + } + + @Override + public SslHandler newHandler(ByteBufAllocator alloc, Executor delegatedTaskExecutor) { + throw new UnsupportedOperationException(); + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) { + throw new UnsupportedOperationException(); + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls) { + throw new UnsupportedOperationException(); + } + + @Override + public SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, + Executor delegatedTaskExecutor) { + throw new UnsupportedOperationException(); + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, + boolean startTls, Executor delegatedTaskExecutor) { + throw new UnsupportedOperationException(); + } + + @Override + protected void finalize() throws Throwable { + try { + nativeSslContext.release(); + } finally { + super.finalize(); + } + } + + void setSessionTimeout(int seconds) throws IllegalArgumentException { + if (sessionCache != null) { + sessionCache.setSessionTimeout(seconds); + } else { + BoringSSL.SSLContext_setSessionCacheTimeout(nativeSslContext.address(), seconds); + this.sessionTimeout = seconds; + } + } + + void setSessionCacheSize(int size) throws IllegalArgumentException { + if (sessionCache != null) { + sessionCache.setSessionCacheSize(size); + } else { + BoringSSL.SSLContext_setSessionCacheSize(nativeSslContext.address(), size); + sessionCacheSize = size; + } + } + + void setSessionTicketKeys(SslSessionTicketKey[] ticketKeys) { + sessionTicketCallback.setSessionTicketKeys(ticketKeys); + BoringSSL.SSLContext_setSessionTicketKeys( + nativeSslContext.address(), ticketKeys != null && ticketKeys.length != 0); + } + + @SuppressWarnings("deprecation") + private static final class QuicheQuicApplicationProtocolNegotiator implements ApplicationProtocolNegotiator { + private final List protocols; + + QuicheQuicApplicationProtocolNegotiator(String... protocols) { + if (protocols == null) { + this.protocols = Collections.emptyList(); + } else { + this.protocols = Collections.unmodifiableList(Arrays.asList(protocols)); + } + } + + @Override + public List protocols() { + return protocols; + } + } + + private static final class QuicheQuicSslSessionContext implements QuicSslSessionContext { + private final QuicheQuicSslContext context; + + QuicheQuicSslSessionContext(QuicheQuicSslContext context) { + this.context = context; + } + + @Override + public SSLSession getSession(byte[] sessionId) { + return null; + } + + @Override + public Enumeration getIds() { + return new Enumeration() { + @Override + public boolean hasMoreElements() { + return false; + } + + @Override + public byte[] nextElement() { + throw new NoSuchElementException(); + } + }; + } + + @Override + public void setSessionTimeout(int seconds) throws IllegalArgumentException { + context.setSessionTimeout(seconds); + } + + @Override + public int getSessionTimeout() { + return (int) context.sessionTimeout(); + } + + @Override + public void setSessionCacheSize(int size) throws IllegalArgumentException { + context.setSessionCacheSize(size); + } + + @Override + public int getSessionCacheSize() { + return (int) context.sessionCacheSize(); + } + + @Override + public void setTicketKeys(SslSessionTicketKey... keys) { + context.setSessionTicketKeys(keys); + } + } + + static final class NativeSslContext extends AbstractReferenceCounted { + private final long ctx; + + NativeSslContext(long ctx) { + this.ctx = ctx; + } + + long address() { + return ctx; + } + + @Override + protected void deallocate() { + BoringSSL.SSLContext_free(ctx); + } + + @Override + public ReferenceCounted touch(Object hint) { + return this; + } + + @Override + public String toString() { + return "NativeSslContext{" + + "ctx=" + ctx + + '}'; + } + } + + private static final class BoringSSLAsyncPrivateKeyMethodAdapter implements BoringSSLPrivateKeyMethod { + private final QuicheQuicSslEngineMap engineMap; + private final BoringSSLAsyncPrivateKeyMethod privateKeyMethod; + + BoringSSLAsyncPrivateKeyMethodAdapter(QuicheQuicSslEngineMap engineMap, + BoringSSLAsyncPrivateKeyMethod privateKeyMethod) { + this.engineMap = engineMap; + this.privateKeyMethod = privateKeyMethod; + } + + @Override + public void sign(long ssl, int signatureAlgorithm, byte[] input, BiConsumer callback) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + callback.accept(null, null); + } else { + privateKeyMethod.sign(engine, signatureAlgorithm, input).addListener(f -> { + Throwable cause = f.cause(); + if (cause != null) { + callback.accept(null, cause); + } else { + callback.accept((byte[]) f.getNow(), null); + } + }); + } + } + + @Override + public void decrypt(long ssl, byte[] input, BiConsumer callback) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + callback.accept(null, null); + } else { + privateKeyMethod.decrypt(engine, input).addListener(f -> { + Throwable cause = f.cause(); + if (cause != null) { + callback.accept(null, cause); + } else { + callback.accept((byte[]) f.getNow(), null); + } + }); + } + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngine.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngine.java new file mode 100644 index 0000000..fd68cbf --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngine.java @@ -0,0 +1,567 @@ +/* + * 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.handler.codec.quic; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.util.LazyJavaxX509Certificate; +import io.netty.handler.ssl.util.LazyX509Certificate; +import io.netty.util.NetUtil; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionBindingEvent; +import javax.net.ssl.SSLSessionBindingListener; +import javax.net.ssl.SSLSessionContext; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.LongFunction; + +final class QuicheQuicSslEngine extends QuicSslEngine { + QuicheQuicSslContext ctx; + private final String peerHost; + private final int peerPort; + private final QuicheQuicSslSession session = new QuicheQuicSslSession(); + private volatile Certificate[] localCertificateChain; + private List sniHostNames; + private boolean handshakeFinished; + private String applicationProtocol; + private boolean sessionReused; + final String tlsHostName; + volatile QuicheQuicConnection connection; + + String sniHostname; + + QuicheQuicSslEngine(QuicheQuicSslContext ctx, String peerHost, int peerPort) { + this.ctx = ctx; + this.peerHost = peerHost; + this.peerPort = peerPort; + // Use SNI if peerHost was specified and a valid hostname + // See https://github.com/netty/netty/issues/4746 + if (ctx.isClient() && isValidHostNameForSNI(peerHost)) { + tlsHostName = peerHost; + sniHostNames = Collections.singletonList(new SNIHostName(tlsHostName)); + } else { + tlsHostName = null; + } + } + + long moveTo(String hostname, QuicheQuicSslContext ctx) { + // First of remove the engine from its previous QuicheQuicSslContext. + this.ctx.remove(this); + this.ctx = ctx; + long added = ctx.add(this); + sniHostname = hostname; + return added; + } + + QuicheQuicConnection createConnection(LongFunction connectionCreator) { + return ctx.createConnection(connectionCreator, this); + } + + void setLocalCertificateChain(Certificate[] localCertificateChain) { + this.localCertificateChain = localCertificateChain; + } + + /** + * Validate that the given hostname can be used in SNI extension. + */ + static boolean isValidHostNameForSNI(String hostname) { + return hostname != null && + hostname.indexOf('.') > 0 && + !hostname.endsWith(".") && + !NetUtil.isValidIpV4Address(hostname) && + !NetUtil.isValidIpV6Address(hostname); + } + + @Override + public SSLParameters getSSLParameters() { + SSLParameters parameters = super.getSSLParameters(); + parameters.setServerNames(sniHostNames); + return parameters; + } + + // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier + // java8 version we don't use @Override annotations here. + public synchronized String getApplicationProtocol() { + return applicationProtocol; + } + + // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier + // java8 version we don't use @Override annotations here. + public synchronized String getHandshakeApplicationProtocol() { + return applicationProtocol; + } + + @Override + public SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst) { + throw new UnsupportedOperationException(); + } + + @Override + public SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public Runnable getDelegatedTask() { + return null; + } + + @Override + public void closeInbound() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInboundDone() { + return false; + } + + @Override + public void closeOutbound() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOutboundDone() { + return false; + } + + @Override + public String[] getSupportedCipherSuites() { + return ctx.cipherSuites().toArray(new String[0]); + } + + @Override + public String[] getEnabledCipherSuites() { + return getSupportedCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getSupportedProtocols() { + // QUIC only supports TLSv1.3 + return new String[] { "TLSv1.3" }; + } + + @Override + public String[] getEnabledProtocols() { + return getSupportedProtocols(); + } + + @Override + public void setEnabledProtocols(String[] protocols) { + throw new UnsupportedOperationException(); + } + + @Override + public SSLSession getSession() { + return session; + } + + @Override + public SSLSession getHandshakeSession() { + if (handshakeFinished) { + return null; + } + return session; + } + + @Override + public void beginHandshake() { + // NOOP + } + + @Override + public SSLEngineResult.HandshakeStatus getHandshakeStatus() { + if (handshakeFinished) { + return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; + } + return SSLEngineResult.HandshakeStatus.NEED_WRAP; + } + + @Override + public void setUseClientMode(boolean clientMode) { + if (clientMode != ctx.isClient()) { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean getUseClientMode() { + return ctx.isClient(); + } + + @Override + public void setNeedClientAuth(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getNeedClientAuth() { + return ctx.clientAuth == ClientAuth.REQUIRE; + } + + @Override + public void setWantClientAuth(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getWantClientAuth() { + return ctx.clientAuth == ClientAuth.OPTIONAL; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getEnableSessionCreation() { + return false; + } + + synchronized void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, + long creationTime, long timeout, + byte[] applicationProtocol, boolean sessionReused) { + if (applicationProtocol == null) { + this.applicationProtocol = null; + } else { + this.applicationProtocol = new String(applicationProtocol); + } + session.handshakeFinished(id, cipher, protocol, peerCertificate, peerCertificateChain, creationTime, timeout); + this.sessionReused = sessionReused; + handshakeFinished = true; + } + + void removeSessionFromCacheIfInvalid() { + session.removeFromCacheIfInvalid(); + } + + synchronized boolean isSessionReused() { + return sessionReused; + } + + private final class QuicheQuicSslSession implements SSLSession { + private Certificate[] x509PeerCerts; + private Certificate[] peerCerts; + private String protocol; + private String cipher; + private byte[] id; + private long creationTime = -1; + private long timeout = -1; + private boolean invalid; + private long lastAccessedTime = -1; + + // lazy init for memory reasons + private Map values; + + private boolean isEmpty(Object[] arr) { + return arr == null || arr.length == 0; + } + private boolean isEmpty(byte[] arr) { + return arr == null || arr.length == 0; + } + + void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout) { + synchronized (QuicheQuicSslEngine.this) { + initPeerCerts(peerCertificateChain, peerCertificate); + this.id = id; + this.cipher = cipher; + 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()); + } + } + + /** + * Init peer certificates that can be obtained via {@link #getPeerCertificateChain()} + * and {@link #getPeerCertificates()}. + */ + private void initPeerCerts(byte[][] chain, byte[] clientCert) { + // Return the full chain from the JNI layer. + if (getUseClientMode()) { + if (isEmpty(chain)) { + peerCerts = EmptyArrays.EMPTY_CERTIFICATES; + x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; + } else { + peerCerts = new Certificate[chain.length]; + x509PeerCerts = new X509Certificate[chain.length]; + initCerts(chain, 0); + } + } else { + // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer + // certificate. We use SSL_get_peer_certificate to get it in this case and add it to our + // array later. + // + // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html + if (isEmpty(clientCert)) { + peerCerts = EmptyArrays.EMPTY_CERTIFICATES; + x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; + } else { + if (isEmpty(chain)) { + peerCerts = new Certificate[] {new LazyX509Certificate(clientCert)}; + x509PeerCerts = new X509Certificate[] {new LazyJavaxX509Certificate(clientCert)}; + } else { + peerCerts = new Certificate[chain.length + 1]; + x509PeerCerts = new X509Certificate[chain.length + 1]; + peerCerts[0] = new LazyX509Certificate(clientCert); + x509PeerCerts[0] = new LazyJavaxX509Certificate(clientCert); + initCerts(chain, 1); + } + } + } + } + + private void initCerts(byte[][] chain, int startPos) { + for (int i = 0; i < chain.length; i++) { + int certPos = startPos + i; + peerCerts[certPos] = new LazyX509Certificate(chain[i]); + x509PeerCerts[certPos] = new LazyJavaxX509Certificate(chain[i]); + } + } + + @Override + public byte[] getId() { + synchronized (QuicheQuicSslSession.this) { + if (id == null) { + return EmptyArrays.EMPTY_BYTES; + } + return id.clone(); + } + } + + @Override + public SSLSessionContext getSessionContext() { + return ctx.sessionContext(); + } + + @Override + public long getCreationTime() { + synchronized (QuicheQuicSslEngine.this) { + return creationTime; + } + } + + @Override + public long getLastAccessedTime() { + return lastAccessedTime; + } + + @Override + public void invalidate() { + boolean removeFromCache; + synchronized (this) { + removeFromCache = !invalid; + invalid = true; + } + if (removeFromCache) { + removeFromCache(); + } + } + + @Override + public boolean isValid() { + synchronized (QuicheQuicSslEngine.this) { + return !invalid && System.currentTimeMillis() - timeout < creationTime; + } + } + + @Override + public void putValue(String name, Object value) { + ObjectUtil.checkNotNull(name, "name"); + ObjectUtil.checkNotNull(value, "value"); + + final Object old; + synchronized (this) { + Map values = this.values; + if (values == null) { + // Use size of 2 to keep the memory overhead small + values = this.values = new HashMap<>(2); + } + old = values.put(name, value); + } + + if (value instanceof SSLSessionBindingListener) { + // Use newSSLSessionBindingEvent so we alway use the wrapper if needed. + ((SSLSessionBindingListener) value).valueBound(newSSLSessionBindingEvent(name)); + } + notifyUnbound(old, name); + } + + @Override + public Object getValue(String name) { + ObjectUtil.checkNotNull(name, "name"); + synchronized (this) { + if (values == null) { + return null; + } + return values.get(name); + } + } + + @Override + public void removeValue(String name) { + ObjectUtil.checkNotNull(name, "name"); + + final Object old; + synchronized (this) { + Map values = this.values; + if (values == null) { + return; + } + old = values.remove(name); + } + + notifyUnbound(old, name); + } + + @Override + public String[] getValueNames() { + synchronized (this) { + Map values = this.values; + if (values == null || values.isEmpty()) { + return EmptyArrays.EMPTY_STRINGS; + } + return values.keySet().toArray(new String[0]); + } + } + + private SSLSessionBindingEvent newSSLSessionBindingEvent(String name) { + return new SSLSessionBindingEvent(session, name); + } + + private void notifyUnbound(Object value, String name) { + if (value instanceof SSLSessionBindingListener) { + // Use newSSLSessionBindingEvent so we alway use the wrapper if needed. + ((SSLSessionBindingListener) value).valueUnbound(newSSLSessionBindingEvent(name)); + } + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + synchronized (QuicheQuicSslEngine.this) { + if (isEmpty(peerCerts)) { + throw new SSLPeerUnverifiedException("peer not verified"); + } + return peerCerts.clone(); + } + } + + @Override + public Certificate[] getLocalCertificates() { + Certificate[] localCerts = localCertificateChain; + if (localCerts == null) { + return null; + } + return localCerts.clone(); + } + + /** + * @deprecated use getPeerCertificates() instead + * @return UnsupportedOperationException + * @throws SSLPeerUnverifiedException + */ + @Deprecated + @Override + public javax.security.cert.X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { + // JDK 17+ may throw unsupported operation because javax.security.cert.X509Certificate is not present + throw new UnsupportedOperationException(); + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + Certificate[] peer = getPeerCertificates(); + // No need for null or length > 0 is needed as this is done in getPeerCertificates() + // already. + return ((java.security.cert.X509Certificate) peer[0]).getSubjectX500Principal(); + } + + @Override + public Principal getLocalPrincipal() { + Certificate[] local = localCertificateChain; + if (local == null || local.length == 0) { + return null; + } + return ((java.security.cert.X509Certificate) local[0]).getIssuerX500Principal(); + } + + @Override + public String getCipherSuite() { + return cipher; + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public String getPeerHost() { + return peerHost; + } + + @Override + public int getPeerPort() { + return peerPort; + } + + @Override + public int getPacketBufferSize() { + return -1; + } + + @Override + public int getApplicationBufferSize() { + return -1; + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngineMap.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngineMap.java new file mode 100644 index 0000000..4ce18f5 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngineMap.java @@ -0,0 +1,37 @@ +/* + * 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.handler.codec.quic; + + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +final class QuicheQuicSslEngineMap { + + private final ConcurrentMap engines = new ConcurrentHashMap<>(); + + QuicheQuicSslEngine get(long ssl) { + return engines.get(ssl); + } + + QuicheQuicSslEngine remove(long ssl) { + return engines.remove(ssl); + } + + void put(long ssl, QuicheQuicSslEngine engine) { + engines.put(ssl, engine); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannel.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannel.java new file mode 100644 index 0000000..fb34ea0 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannel.java @@ -0,0 +1,956 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelId; +import io.netty.channel.DefaultChannelPipeline; +import io.netty.channel.EventLoop; +import io.netty.channel.PendingWriteQueue; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.VoidChannelPromise; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.ChannelOutputShutdownException; +import io.netty.util.DefaultAttributeMap; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.PromiseNotifier; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.RejectedExecutionException; + +/** + * {@link QuicStreamChannel} implementation that uses quiche. + */ +final class QuicheQuicStreamChannel extends DefaultAttributeMap implements QuicStreamChannel { + private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicheQuicStreamChannel.class); + private final QuicheQuicChannel parent; + private final ChannelId id; + private final ChannelPipeline pipeline; + private final QuicStreamChannelUnsafe unsafe; + private final ChannelPromise closePromise; + private final PendingWriteQueue queue; + + private final QuicStreamChannelConfig config; + private final QuicStreamAddress address; + + private boolean readable; + private boolean readPending; + private boolean inRecv; + private boolean inWriteQueued; + private boolean finReceived; + private boolean finSent; + + private volatile boolean registered; + private volatile boolean writable = true; + private volatile boolean active = true; + private volatile boolean inputShutdown; + private volatile boolean outputShutdown; + private volatile QuicStreamPriority priority; + private volatile int capacity; + + QuicheQuicStreamChannel(QuicheQuicChannel parent, long streamId) { + this.parent = parent; + this.id = DefaultChannelId.newInstance(); + unsafe = new QuicStreamChannelUnsafe(); + this.pipeline = new DefaultChannelPipeline(this) { + // TODO: add some overrides maybe ? + }; + config = new QuicheQuicStreamChannelConfig(this); + this.address = new QuicStreamAddress(streamId); + this.closePromise = newPromise(); + queue = new PendingWriteQueue(this); + // Local created unidirectional streams have the input shutdown by spec. There will never be any data for + // these to be read. + if (parent.streamType(streamId) == QuicStreamType.UNIDIRECTIONAL && parent.isStreamLocalCreated(streamId)) { + inputShutdown = true; + } + } + + @Override + public QuicStreamAddress localAddress() { + return address; + } + + @Override + public QuicStreamAddress remoteAddress() { + return address; + } + + @Override + public boolean isLocalCreated() { + return parent().isStreamLocalCreated(streamId()); + } + + @Override + public QuicStreamType type() { + return parent().streamType(streamId()); + } + + @Override + public long streamId() { + return address.streamId(); + } + + @Override + public QuicStreamPriority priority() { + return priority; + } + + @Override + public ChannelFuture updatePriority(QuicStreamPriority priority, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + updatePriority0(priority, promise); + } else { + eventLoop().execute(() -> updatePriority0(priority, promise)); + } + return promise; + } + + private void updatePriority0(QuicStreamPriority priority, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + try { + parent().streamPriority(streamId(), (byte) priority.urgency(), priority.isIncremental()); + } catch (Throwable cause) { + promise.setFailure(cause); + return; + } + this.priority = priority; + promise.setSuccess(); + } + + @Override + public boolean isInputShutdown() { + return inputShutdown; + } + + @Override + public ChannelFuture shutdownOutput(ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdownOutput0(promise); + } else { + eventLoop().execute(() -> shutdownOutput0(promise)); + } + return promise; + } + + private void shutdownOutput0(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + outputShutdown = true; + unsafe.writeWithoutCheckChannelState(QuicStreamFrame.EMPTY_FIN, promise); + unsafe.flush(); + } + + @Override + public ChannelFuture shutdownInput(int error, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdownInput0(error, promise); + } else { + eventLoop().execute(() -> shutdownInput0(error, promise)); + } + return promise; + } + + @Override + public ChannelFuture shutdownOutput(int error, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdownOutput0(error, promise); + } else { + eventLoop().execute(() -> shutdownOutput0(error, promise)); + } + return promise; + } + + @Override + public QuicheQuicChannel parent() { + return parent; + } + + private void shutdownInput0(int err, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + inputShutdown = true; + parent().streamShutdown(streamId(), true, false, err, channelPromise); + closeIfDone(); + } + + @Override + public boolean isOutputShutdown() { + return outputShutdown; + } + + private void shutdownOutput0(int error, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + parent().streamShutdown(streamId(), false, true, error, channelPromise); + outputShutdown = true; + closeIfDone(); + } + + @Override + public boolean isShutdown() { + return outputShutdown && inputShutdown; + } + + @Override + public ChannelFuture shutdown(ChannelPromise channelPromise) { + if (eventLoop().inEventLoop()) { + shutdown0(channelPromise); + } else { + eventLoop().execute(() -> shutdown0(channelPromise)); + } + return channelPromise; + } + + private void shutdown0(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + inputShutdown = true; + outputShutdown = true; + unsafe.writeWithoutCheckChannelState(QuicStreamFrame.EMPTY_FIN, unsafe.voidPromise()); + unsafe.flush(); + parent().streamShutdown(streamId(), true, false, 0, promise); + closeIfDone(); + } + + @Override + public ChannelFuture shutdown(int error, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdown0(error, promise); + } else { + eventLoop().execute(() -> shutdown0(error, promise)); + } + return promise; + } + + private void shutdown0(int error, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + inputShutdown = true; + outputShutdown = true; + parent().streamShutdown(streamId(), true, true, error, channelPromise); + closeIfDone(); + } + + private void sendFinIfNeeded() throws Exception { + if (!finSent) { + finSent = true; + parent().streamSendFin(streamId()); + } + } + + private void closeIfDone() { + if (finSent && (finReceived || type() == QuicStreamType.UNIDIRECTIONAL && isLocalCreated())) { + unsafe().close(unsafe().voidPromise()); + } + } + + private void removeStreamFromParent() { + if (!active && finReceived) { + parent().streamClosed(streamId()); + inputShutdown = true; + outputShutdown = true; + } + } + + @Override + public QuicStreamChannel flush() { + pipeline.flush(); + return this; + } + + @Override + public QuicStreamChannel read() { + pipeline.read(); + return this; + } + + @Override + public QuicStreamChannelConfig config() { + return config; + } + + @Override + public boolean isOpen() { + return active; + } + + @Override + public boolean isActive() { + return isOpen(); + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + public ChannelId id() { + return id; + } + + @Override + public EventLoop eventLoop() { + return parent.eventLoop(); + } + + @Override + public boolean isRegistered() { + return registered; + } + + @Override + public ChannelFuture closeFuture() { + return closePromise; + } + + @Override + public boolean isWritable() { + return writable; + } + + @Override + public long bytesBeforeUnwritable() { + // Capacity might be negative if the stream was closed. + return Math.max(capacity, 0); + } + + @Override + public long bytesBeforeWritable() { + if (writable) { + return 0; + } + // Just return something positive for now + return 8; + } + + @Override + public Unsafe unsafe() { + return unsafe; + } + + @Override + public ChannelPipeline pipeline() { + return pipeline; + } + + @Override + public ByteBufAllocator alloc() { + return config.getAllocator(); + } + + @Override + public int compareTo(Channel o) { + return id.compareTo(o.id()); + } + + /** + * Returns the ID of this channel. + */ + @Override + public int hashCode() { + return id.hashCode(); + } + + /** + * Returns {@code true} if and only if the specified object is identical + * with this channel (i.e: {@code this == o}). + */ + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public String toString() { + return "[id: 0x" + id.asShortText() + ", " + address + "]"; + } + + /** + * Stream is writable. + */ + boolean writable(@SuppressWarnings("unused") int capacity) { + assert eventLoop().inEventLoop(); + this.capacity = capacity; + boolean mayNeedWrite = ((QuicStreamChannelUnsafe) unsafe()).writeQueued(); + // we need to re-read this.capacity as writeQueued() may update the capacity. + updateWritabilityIfNeeded(this.capacity > 0); + return mayNeedWrite; + } + + private void updateWritabilityIfNeeded(boolean newWritable) { + if (writable != newWritable) { + writable = newWritable; + pipeline.fireChannelWritabilityChanged(); + } + } + + /** + * Stream is readable. + */ + void readable() { + assert eventLoop().inEventLoop(); + // Mark as readable and if a read is pending execute it. + readable = true; + if (readPending) { + ((QuicStreamChannelUnsafe) unsafe()).recv(); + } + } + + void forceClose() { + assert eventLoop().inEventLoop(); + // Set received to true to ensure we will remove it from the internal map once we send the fin. + finSent = true; + unsafe().close(unsafe().voidPromise()); + } + + private final class QuicStreamChannelUnsafe implements Unsafe { + private RecvByteBufAllocator.Handle recvHandle; + + private final ChannelPromise voidPromise = new VoidChannelPromise( + QuicheQuicStreamChannel.this, false); + @Override + public void connect(SocketAddress remote, SocketAddress local, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + promise.setFailure(new UnsupportedOperationException()); + } + + @SuppressWarnings("deprecation") + @Override + public RecvByteBufAllocator.Handle recvBufAllocHandle() { + if (recvHandle == null) { + recvHandle = config.getRecvByteBufAllocator().newHandle(); + } + return recvHandle; + } + + @Override + public SocketAddress localAddress() { + return address; + } + + @Override + public SocketAddress remoteAddress() { + return address; + } + + @Override + public void register(EventLoop eventLoop, ChannelPromise promise) { + assert eventLoop.inEventLoop(); + if (registered) { + promise.setFailure(new IllegalStateException()); + return; + } + if (eventLoop != parent.eventLoop()) { + promise.setFailure(new IllegalArgumentException()); + return; + } + registered = true; + promise.setSuccess(); + pipeline.fireChannelRegistered(); + pipeline.fireChannelActive(); + } + + @Override + public void bind(SocketAddress localAddress, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + promise.setFailure(new UnsupportedOperationException()); + } + + @Override + public void disconnect(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + close(promise); + } + + @Override + public void close(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + if (!active || closePromise.isDone()) { + if (promise.isVoid()) { + return; + } + closePromise.addListener(new PromiseNotifier<>(promise)); + return; + } + active = false; + try { + // Close the channel and fail the queued messages in all cases. + sendFinIfNeeded(); + } catch (Exception ignore) { + // Just ignore + } finally { + if (!queue.isEmpty()) { + // Only fail if the queue is non-empty. + queue.removeAndFailAll(new ClosedChannelException()); + } + + promise.trySuccess(); + closePromise.trySuccess(); + if (type() == QuicStreamType.UNIDIRECTIONAL && isLocalCreated()) { + inputShutdown = true; + outputShutdown = true; + // If its an unidirectional stream and was created locally it is safe to close the stream now as + // we will never receive data from the other side. + parent().streamClosed(streamId()); + } else { + removeStreamFromParent(); + } + } + if (inWriteQueued) { + invokeLater(() -> deregister(voidPromise(), true)); + } else { + deregister(voidPromise(), true); + } + } + + private void deregister(final ChannelPromise promise, final boolean fireChannelInactive) { + assert eventLoop().inEventLoop(); + if (!promise.setUncancellable()) { + return; + } + + if (!registered) { + promise.trySuccess(); + return; + } + + // As a user may call deregister() from within any method while doing processing in the ChannelPipeline, + // we need to ensure we do the actual deregister operation later. This is needed as for example, + // we may be in the ByteToMessageDecoder.callDecode(...) method and so still try to do processing in + // the old EventLoop while the user already registered the Channel to a new EventLoop. Without delay, + // the deregister operation this could lead to have a handler invoked by different EventLoop and so + // threads. + // + // See: + // https://github.com/netty/netty/issues/4435 + invokeLater(() -> { + if (fireChannelInactive) { + pipeline.fireChannelInactive(); + } + // Some transports like local and AIO does not allow the deregistration of + // an open channel. Their doDeregister() calls close(). Consequently, + // close() calls deregister() again - no need to fire channelUnregistered, so check + // if it was registered. + if (registered) { + registered = false; + pipeline.fireChannelUnregistered(); + } + promise.setSuccess(); + }); + } + + private void invokeLater(Runnable task) { + try { + // This method is used by outbound operation implementations to trigger an inbound event later. + // They do not trigger an inbound event immediately because an outbound operation might have been + // triggered by another inbound event handler method. If fired immediately, the call stack + // will look like this for example: + // + // handlerA.inboundBufferUpdated() - (1) an inbound handler method closes a connection. + // -> handlerA.ctx.close() + // -> channel.unsafe.close() + // -> handlerA.channelInactive() - (2) another inbound handler method called while in (1) yet + // + // which means the execution of two inbound handler methods of the same handler overlap undesirably. + eventLoop().execute(task); + } catch (RejectedExecutionException e) { + LOGGER.warn("Can't invoke task later as EventLoop rejected it", e); + } + } + + @Override + public void closeForcibly() { + assert eventLoop().inEventLoop(); + close(unsafe().voidPromise()); + } + + @Override + public void deregister(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + deregister(promise, false); + } + + @Override + public void beginRead() { + assert eventLoop().inEventLoop(); + readPending = true; + if (readable) { + ((QuicStreamChannelUnsafe) unsafe()).recv(); + + // As the stream was readable, and we called recv() ourselves we also need to call + // connectionSendAndFlush(). This is needed as recv() might consume data and so a window update + // frame might be produced. If we miss to call connectionSendAndFlush() we might never send the update + // to the remote peer and so the remote peer might never attempt to send more data. + // See also https://docs.rs/quiche/latest/quiche/struct.Connection.html#method.send. + parent().connectionSendAndFlush(); + } + } + + private void closeIfNeeded(boolean wasFinSent) { + // Let's check if we should close the channel now. + // If it's a unidirectional channel we can close it as there will be no fin that we can read + // from the remote peer. If its an bidirectional channel we should only close the channel if we + // also received the fin from the remote peer. + if (!wasFinSent && QuicheQuicStreamChannel.this.finSent + && (type() == QuicStreamType.UNIDIRECTIONAL || finReceived)) { + // close the channel now + close(voidPromise()); + } + } + + boolean writeQueued() { + assert eventLoop().inEventLoop(); + boolean wasFinSent = QuicheQuicStreamChannel.this.finSent; + inWriteQueued = true; + try { + if (queue.isEmpty()) { + return false; + } + boolean written = false; + for (;;) { + Object msg = queue.current(); + if (msg == null) { + break; + } + try { + if (!write0(msg)) { + return written; + } + } catch (Exception e) { + if (e instanceof QuicException && ( + (QuicException) e).error() == QuicError.STREAM_STOPPED) { + // Once its signaled that the stream is stopped we can just fail everything. + queue.removeAndFailAll(e); + forceClose(); + break; + } + queue.remove().setFailure(e); + continue; + } + queue.remove().setSuccess(); + written = true; + } + updateWritabilityIfNeeded(true); + return written; + } finally { + closeIfNeeded(wasFinSent); + inWriteQueued = false; + } + } + + @Override + public void write(Object msg, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + + // Check first if the Channel is in a state in which it will accept writes, if not fail everything + // with the right exception + if (!isOpen()) { + queueAndFailAll(msg, promise, new ClosedChannelException()); + } else if (finSent) { + queueAndFailAll(msg, promise, new ChannelOutputShutdownException("Fin was sent already")); + } else { + writeWithoutCheckChannelState(msg, promise); + } + } + + private void queueAndFailAll(Object msg, ChannelPromise promise, Throwable cause) { + queue.add(msg, promise); + queue.removeAndFailAll(cause); + } + + void writeWithoutCheckChannelState(Object msg, ChannelPromise promise) { + if (msg instanceof ByteBuf) { + ByteBuf buffer = (ByteBuf) msg; + if (!buffer.isDirect()) { + ByteBuf tmpBuffer = alloc().directBuffer(buffer.readableBytes()); + tmpBuffer.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes()); + buffer.release(); + msg = tmpBuffer; + } + } else if (msg instanceof QuicStreamFrame) { + QuicStreamFrame frame = (QuicStreamFrame) msg; + ByteBuf buffer = frame.content(); + if (!buffer.isDirect()) { + ByteBuf tmpBuffer = alloc().directBuffer(buffer.readableBytes()); + tmpBuffer.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes()); + buffer.release(); + msg = frame.replace(tmpBuffer); + } + } else { + ReferenceCountUtil.release(msg); + promise.setFailure(new UnsupportedOperationException( + "unsupported message type: " + StringUtil.simpleClassName(msg))); + return; + } + + boolean wasFinSent = QuicheQuicStreamChannel.this.finSent; + boolean mayNeedWritabilityUpdate = false; + try { + if (write0(msg)) { + ReferenceCountUtil.release(msg); + promise.setSuccess(); + mayNeedWritabilityUpdate = capacity == 0; + } else { + queue.add(msg, promise); + mayNeedWritabilityUpdate = true; + } + } catch (Exception e) { + ReferenceCountUtil.release(msg); + promise.setFailure(e); + mayNeedWritabilityUpdate = capacity == 0; + } finally { + if (mayNeedWritabilityUpdate) { + updateWritabilityIfNeeded(false); + } + closeIfNeeded(wasFinSent); + } + } + + private boolean write0(Object msg) throws Exception { + if (type() == QuicStreamType.UNIDIRECTIONAL && !isLocalCreated()) { + throw new UnsupportedOperationException( + "Writes on non-local created streams that are unidirectional are not supported"); + } + if (finSent) { + throw new ChannelOutputShutdownException("Fin was sent already"); + } + + final boolean fin; + ByteBuf buffer; + if (msg instanceof ByteBuf) { + fin = false; + buffer = (ByteBuf) msg; + } else { + QuicStreamFrame frame = (QuicStreamFrame) msg; + fin = frame.hasFin(); + buffer = frame.content(); + } + + boolean readable = buffer.isReadable(); + if (!fin && !readable) { + return true; + } + + boolean sendSomething = false; + try { + do { + int res = parent().streamSend(streamId(), buffer, fin); + + // Update the capacity as well. + int cap = parent.streamCapacity(streamId()); + if (cap >= 0) { + capacity = cap; + } + if (Quiche.throwIfError(res) || (readable && res == 0)) { + return false; + } + sendSomething = true; + buffer.skipBytes(res); + } while (buffer.isReadable()); + + if (fin) { + finSent = true; + outputShutdown = true; + } + return true; + } finally { + // As we called quiche_conn_stream_send(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + if (sendSomething) { + parent.connectionSendAndFlush(); + } + } + } + + @Override + public void flush() { + assert eventLoop().inEventLoop(); + // NOOP. + } + + @Override + public ChannelPromise voidPromise() { + assert eventLoop().inEventLoop(); + return voidPromise; + } + + @Override + public ChannelOutboundBuffer outboundBuffer() { + return null; + } + + private void closeOnRead(ChannelPipeline pipeline, boolean readFrames) { + if (readFrames && finReceived && finSent) { + close(voidPromise()); + } else if (config.isAllowHalfClosure()) { + if (finReceived) { + // If we receive a fin there will be no more data to read so we need to fire both events + // to be consistent with other transports. + pipeline.fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE); + pipeline.fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE); + if (finSent) { + // This was an unidirectional stream which means as soon as we received FIN and sent a FIN + // we need close the connection. + close(voidPromise()); + } + } + } else { + // This was an unidirectional stream which means as soon as we received FIN we need + // close the connection. + close(voidPromise()); + } + } + + private void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf, Throwable cause, + @SuppressWarnings("deprecation") RecvByteBufAllocator.Handle allocHandle, + boolean readFrames) { + if (byteBuf != null) { + if (byteBuf.isReadable()) { + pipeline.fireChannelRead(byteBuf); + } else { + byteBuf.release(); + } + } + + readComplete(allocHandle, pipeline); + pipeline.fireExceptionCaught(cause); + if (finReceived) { + closeOnRead(pipeline, readFrames); + } + } + + void recv() { + assert eventLoop().inEventLoop(); + if (inRecv) { + // As the use may call read() we need to guard against re-entrancy here as otherwise it could + // be possible that we re-enter this method while still processing it. + return; + } + + inRecv = true; + try { + ChannelPipeline pipeline = pipeline(); + QuicheQuicStreamChannelConfig config = (QuicheQuicStreamChannelConfig) config(); + // Directly access the DirectIoByteBufAllocator as we need an direct buffer to read into in all cases + // even if there is no Unsafe present and the direct buffer is not pooled. + DirectIoByteBufAllocator allocator = config.allocator; + @SuppressWarnings("deprecation") + RecvByteBufAllocator.Handle allocHandle = this.recvBufAllocHandle(); + boolean readFrames = config.isReadFrames(); + + // We should loop as long as a read() was requested and there is anything left to read, which means the + // stream was marked as readable before. + while (active && readPending && readable) { + allocHandle.reset(config); + ByteBuf byteBuf = null; + QuicheQuicChannel parent = parent(); + // It's possible that the stream was marked as finish while we iterated over the readable streams + // or while we did have auto read disabled. If so we need to ensure we not try to read from it as it + // would produce an error. + boolean readCompleteNeeded = false; + boolean continueReading = true; + try { + while (!finReceived && continueReading) { + byteBuf = allocHandle.allocate(allocator); + allocHandle.attemptedBytesRead(byteBuf.writableBytes()); + switch (parent.streamRecv(streamId(), byteBuf)) { + case DONE: + // Nothing left to read; + readable = false; + break; + case FIN: + // If we received a FIN we also should mark the channel as non readable as + // there is nothing left to read really. + readable = false; + finReceived = true; + inputShutdown = true; + break; + case OK: + break; + default: + throw new Error(); + } + allocHandle.lastBytesRead(byteBuf.readableBytes()); + if (allocHandle.lastBytesRead() <= 0) { + byteBuf.release(); + if (finReceived && readFrames) { + // If we read QuicStreamFrames we should fire an frame through the pipeline + // with an empty buffer but the fin flag set to true. + byteBuf = Unpooled.EMPTY_BUFFER; + } else { + byteBuf = null; + break; + } + } + // We did read one message. + allocHandle.incMessagesRead(1); + readCompleteNeeded = true; + + // It's important that we reset this to false before we call fireChannelRead(...) + // as the user may request another read() from channelRead(...) callback. + readPending = false; + + if (readFrames) { + pipeline.fireChannelRead(new DefaultQuicStreamFrame(byteBuf, finReceived)); + } else { + pipeline.fireChannelRead(byteBuf); + } + byteBuf = null; + continueReading = allocHandle.continueReading(); + } + + if (readCompleteNeeded) { + readComplete(allocHandle, pipeline); + } + if (finReceived) { + readable = false; + closeOnRead(pipeline, readFrames); + } + } catch (Throwable cause) { + readable = false; + handleReadException(pipeline, byteBuf, cause, allocHandle, readFrames); + } + } + } finally { + // About to leave the method lets reset so we can enter it again. + inRecv = false; + removeStreamFromParent(); + } + } + + // Read was complete and something was read, so we we need to reset the readPending flags, the allocHandle + // and call fireChannelReadComplete(). The user may schedule another read now. + private void readComplete(@SuppressWarnings("deprecation") RecvByteBufAllocator.Handle allocHandle, + ChannelPipeline pipeline) { + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannelConfig.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannelConfig.java new file mode 100644 index 0000000..917a730 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannelConfig.java @@ -0,0 +1,175 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelOption; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; + +import java.util.Map; + +final class QuicheQuicStreamChannelConfig extends DefaultChannelConfig implements QuicStreamChannelConfig { + // We should use half-closure sementatics by default as this is what QUIC does by default. + // If you receive a FIN you should still keep the stream open until you write a FIN as well. + private volatile boolean allowHalfClosure = true; + private volatile boolean readFrames; + volatile DirectIoByteBufAllocator allocator; + + QuicheQuicStreamChannelConfig(QuicStreamChannel channel) { + super(channel); + allocator = new DirectIoByteBufAllocator(super.getAllocator()); + } + + @Override + public Map, Object> getOptions() { + if (isHalfClosureSupported()) { + return getOptions(super.getOptions(), ChannelOption.ALLOW_HALF_CLOSURE, QuicChannelOption.READ_FRAMES); + } + return super.getOptions(); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == ChannelOption.ALLOW_HALF_CLOSURE) { + return (T) Boolean.valueOf(isAllowHalfClosure()); + } + if (option == QuicChannelOption.READ_FRAMES) { + return (T) Boolean.valueOf(isReadFrames()); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == ChannelOption.ALLOW_HALF_CLOSURE) { + if (isHalfClosureSupported()) { + setAllowHalfClosure((Boolean) value); + return true; + } + return false; + } + if (option == QuicChannelOption.READ_FRAMES) { + setReadFrames((Boolean) value); + } + return super.setOption(option, value); + } + + @Override + public QuicStreamChannelConfig setReadFrames(boolean readFrames) { + this.readFrames = readFrames; + return this; + } + + @Override + public boolean isReadFrames() { + return readFrames; + } + + @Override + public QuicStreamChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + public QuicStreamChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public QuicStreamChannelConfig setAllocator(ByteBufAllocator allocator) { + this.allocator = new DirectIoByteBufAllocator(allocator); + return this; + } + + @Override + public QuicStreamChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public QuicStreamChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public QuicStreamChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + @Override + public QuicStreamChannelConfig setAllowHalfClosure(boolean allowHalfClosure) { + if (!isHalfClosureSupported()) { + throw new UnsupportedOperationException("Undirectional streams don't support half-closure"); + } + this.allowHalfClosure = allowHalfClosure; + return this; + } + + @Override + public ByteBufAllocator getAllocator() { + return allocator.wrapped(); + } + + @Override + public boolean isAllowHalfClosure() { + return allowHalfClosure; + } + + private boolean isHalfClosureSupported() { + return ((QuicStreamChannel) channel).type() == QuicStreamType.BIDIRECTIONAL; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicTransportParameters.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicTransportParameters.java new file mode 100644 index 0000000..2546db6 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicTransportParameters.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 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.handler.codec.quic; + +import io.netty.util.internal.StringUtil; + +final class QuicheQuicTransportParameters implements QuicTransportParameters { + private final long[] values; + + QuicheQuicTransportParameters(long[] values) { + this.values = values; + } + + @Override + public long maxIdleTimeout() { + return values[0]; + } + + @Override + public long maxUdpPayloadSize() { + return values[1]; + } + + @Override + public long initialMaxData() { + return values[2]; + } + + @Override + public long initialMaxStreamDataBidiLocal() { + return values[3]; + } + + @Override + public long initialMaxStreamDataBidiRemote() { + return values[4]; + } + + @Override + public long initialMaxStreamDataUni() { + return values[5]; + } + + @Override + public long initialMaxStreamsBidi() { + return values[6]; + } + + @Override + public long initialMaxStreamsUni() { + return values[7]; + } + + @Override + public long ackDelayExponent() { + return values[8]; + } + + @Override + public long maxAckDelay() { + return values[9]; + } + + @Override + public boolean disableActiveMigration() { + return values[10] == 1; + } + + @Override + public long activeConnIdLimit() { + return values[11]; + } + + @Override + public long maxDatagramFrameSize() { + return values[12]; + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "[" + + "maxIdleTimeout=" + maxIdleTimeout() + + ", maxUdpPayloadSize=" + maxUdpPayloadSize() + + ", initialMaxData=" + initialMaxData() + + ", initialMaxStreamDataBidiLocal=" + initialMaxStreamDataBidiLocal() + + ", initialMaxStreamDataBidiRemote=" + initialMaxStreamDataBidiRemote() + + ", initialMaxStreamDataUni=" + initialMaxStreamDataUni() + + ", initialMaxStreamsBidi=" + initialMaxStreamsBidi() + + ", initialMaxStreamsUni=" + initialMaxStreamsUni() + + ", ackDelayExponent=" + ackDelayExponent() + + ", maxAckDelay=" + maxAckDelay() + + ", disableActiveMigration=" + disableActiveMigration() + + ", activeConnIdLimit=" + activeConnIdLimit() + + ", maxDatagramFrameSize=" + maxDatagramFrameSize() + + "]"; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheRecvInfo.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheRecvInfo.java new file mode 100644 index 0000000..40d7935 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheRecvInfo.java @@ -0,0 +1,85 @@ +/* + * 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.handler.codec.quic; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + + +/** + * Utility class to handle access to {@code quiche_recv_info}. + */ +final class QuicheRecvInfo { + + private QuicheRecvInfo() { } + + /** + * Set the {@link InetSocketAddress} into the {@code quiche_recv_info} struct. + * + *
+     * typedef struct {
+     *     struct sockaddr *from;
+     *     socklen_t from_len;
+     *     struct sockaddr *to;
+     *     socklen_t to_len;
+     * } quiche_recv_info;
+     * 
+ * + * @param memory the memory of {@code quiche_recv_info}. + * @param from the {@link InetSocketAddress} to write into {@code quiche_recv_info}. + * @param to the {@link InetSocketAddress} to write into {@code quiche_recv_info}. + */ + static void setRecvInfo(ByteBuffer memory, InetSocketAddress from, InetSocketAddress to) { + int position = memory.position(); + try { + setAddress(memory, Quiche.SIZEOF_QUICHE_RECV_INFO, Quiche.QUICHE_RECV_INFO_OFFSETOF_FROM, Quiche.QUICHE_RECV_INFO_OFFSETOF_FROM_LEN, from); + setAddress(memory, Quiche.SIZEOF_QUICHE_RECV_INFO + Quiche.SIZEOF_SOCKADDR_STORAGE, + Quiche.QUICHE_RECV_INFO_OFFSETOF_TO, Quiche.QUICHE_RECV_INFO_OFFSETOF_TO_LEN, to); + } finally { + memory.position(position); + } + } + + private static void setAddress(ByteBuffer memory, int socketAddressOffset, int addrOffset, int lenOffset, InetSocketAddress address) { + int position = memory.position(); + try { + int sockaddrPosition = position +socketAddressOffset; + memory.position(sockaddrPosition); + long sockaddrMemoryAddress = Quiche.memoryAddressWithPosition(memory); + int len = SockaddrIn.setAddress(memory, address); + if (Quiche.SIZEOF_SIZE_T == 4) { + memory.putInt(position + addrOffset, (int) sockaddrMemoryAddress); + } else { + memory.putLong(position + addrOffset, sockaddrMemoryAddress); + } + Quiche.setPrimitiveValue(memory, position + lenOffset, Quiche.SIZEOF_SOCKLEN_T, len); + } finally { + memory.position(position); + } + } + + /** + * Returns {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored. + * + * @param memory the first {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @param memory2 the second {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @return {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored, {@code false} + * otherwise. + */ + static boolean isSameAddress(ByteBuffer memory, ByteBuffer memory2) { + return Quiche.isSameAddress(memory, memory2, Quiche.SIZEOF_QUICHE_RECV_INFO); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheSendInfo.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheSendInfo.java new file mode 100644 index 0000000..b371680 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicheSendInfo.java @@ -0,0 +1,167 @@ +/* + * 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.handler.codec.quic; + +import io.netty.util.concurrent.FastThreadLocal; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +/** + * Utility class to handle access to {@code quiche_send_info}. + */ +final class QuicheSendInfo { + + private static final FastThreadLocal IPV4_ARRAYS = new FastThreadLocal() { + @Override + protected byte[] initialValue() { + return new byte[SockaddrIn.IPV4_ADDRESS_LENGTH]; + } + }; + + private static final FastThreadLocal IPV6_ARRAYS = new FastThreadLocal() { + @Override + protected byte[] initialValue() { + return new byte[SockaddrIn.IPV6_ADDRESS_LENGTH]; + } + }; + + private static final byte[] TIMESPEC_ZEROOUT = new byte[Quiche.SIZEOF_TIMESPEC]; + + private QuicheSendInfo() { } + + /** + * Get the {@link InetSocketAddress} out of the {@code quiche_send_info} struct. + * + * @param memory the memory of {@code quiche_send_info}. + * @return the address that was read. + */ + static InetSocketAddress getToAddress(ByteBuffer memory) { + return getAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO_LEN, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO); + } + + static InetSocketAddress getFromAddress(ByteBuffer memory) { + return getAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM_LEN, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM); + } + + private static InetSocketAddress getAddress(ByteBuffer memory, int lenOffset, int addressOffset) { + int position = memory.position(); + try { + long len = getLen(memory, position + lenOffset); + + memory.position(position + addressOffset); + + if (len == Quiche.SIZEOF_SOCKADDR_IN) { + return SockaddrIn.getIPv4(memory, IPV4_ARRAYS.get()); + } + assert len == Quiche.SIZEOF_SOCKADDR_IN6; + return SockaddrIn.getIPv6(memory, IPV6_ARRAYS.get(), IPV4_ARRAYS.get()); + } finally { + memory.position(position); + } + } + + private static long getLen(ByteBuffer memory, int index) { + return Quiche.getPrimitiveValue(memory, index, Quiche.SIZEOF_SOCKLEN_T); + } + + /** + * Set the {@link InetSocketAddress} into the {@code quiche_send_info} struct. + *
+     *
+     * typedef struct {
+     *     // The local address the packet should be sent from.
+     *     struct sockaddr_storage from;
+     *     socklen_t from_len;
+     *
+     *     // The address the packet should be sent to.
+     *     struct sockaddr_storage to;
+     *     socklen_t to_len;
+     *
+     *     // The time to send the packet out.
+     *     struct timespec at;
+     * } quiche_send_info;
+     * 
+ * + * @param memory the memory of {@code quiche_send_info}. + * @param from the {@link InetSocketAddress} to write into {@code quiche_send_info}. + * @param to the {@link InetSocketAddress} to write into {@code quiche_send_info}. + */ + static void setSendInfo(ByteBuffer memory, InetSocketAddress from, InetSocketAddress to) { + int position = memory.position(); + try { + setAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM_LEN, from); + setAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO_LEN, to); + // Zero out the timespec. + memory.position(position + Quiche.QUICHE_SEND_INFO_OFFSETOF_AT); + memory.put(TIMESPEC_ZEROOUT); + } finally { + memory.position(position); + } + } + + private static void setAddress(ByteBuffer memory, int addrOffset, int lenOffset, InetSocketAddress addr) { + int position = memory.position(); + try { + memory.position(position + addrOffset); + int len = SockaddrIn.setAddress(memory, addr); + Quiche.setPrimitiveValue(memory, position + lenOffset, Quiche.SIZEOF_SOCKLEN_T, len); + } finally { + memory.position(position); + } + } + + /** + * Get the {@code timespec} from the {@code quiche_send_info} struct in nanos. + *
+     *
+     * typedef struct {
+     *     // The local address the packet should be sent from.
+     *     struct sockaddr_storage from;
+     *     socklen_t from_len;
+     *
+     *     // The address the packet should be sent to.
+     *     struct sockaddr_storage to;
+     *     socklen_t to_len;
+     *
+     *     // The time to send the packet out.
+     *     struct timespec at;
+     * } quiche_send_info;
+     * 
+ * + * @param memory the memory of {@code quiche_send_info}. + */ + static long getAtNanos(ByteBuffer memory) { + long sec = Quiche.getPrimitiveValue(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_AT + + Quiche.TIMESPEC_OFFSETOF_TV_SEC, Quiche.SIZEOF_TIME_T); + long nsec = Quiche.getPrimitiveValue(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_AT + + Quiche.TIMESPEC_OFFSETOF_TV_SEC, Quiche.SIZEOF_LONG); + return TimeUnit.SECONDS.toNanos(sec) + nsec; + } + + /** + * Returns {@code true} if both {@link ByteBuffer}s have the same {@code sockaddr_storage} stored. + * + * @param memory the first {@link ByteBuffer} which holds a {@code quiche_send_info}. + * @param memory2 the second {@link ByteBuffer} which holds a {@code quiche_send_info}. + * @return {@code true} if both {@link ByteBuffer}s have the same {@code sockaddr_storage} stored, + * {@code false} otherwise. + */ + static boolean isSameAddress(ByteBuffer memory, ByteBuffer memory2) { + return Quiche.isSameAddress(memory, memory2, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO); + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SecureRandomQuicConnectionIdGenerator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SecureRandomQuicConnectionIdGenerator.java new file mode 100644 index 0000000..622d798 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SecureRandomQuicConnectionIdGenerator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; + +final class SecureRandomQuicConnectionIdGenerator implements QuicConnectionIdGenerator { + private static final SecureRandom RANDOM = new SecureRandom(); + + static final QuicConnectionIdGenerator INSTANCE = new SecureRandomQuicConnectionIdGenerator(); + + private SecureRandomQuicConnectionIdGenerator() { + } + + @Override + public ByteBuffer newId(int length) { + ObjectUtil.checkInRange(length, 0, maxConnectionIdLength(), "length"); + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + return ByteBuffer.wrap(bytes); + } + + @Override + public ByteBuffer newId(ByteBuffer buffer, int length) { + return newId(length); + } + + @Override + public int maxConnectionIdLength() { + return Quiche.QUICHE_MAX_CONN_ID_LEN; + } + + @Override + public boolean isIdempotent() { + return false; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SegmentedDatagramPacketAllocator.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SegmentedDatagramPacketAllocator.java new file mode 100644 index 0000000..0784bf7 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SegmentedDatagramPacketAllocator.java @@ -0,0 +1,63 @@ +/* + * 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.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.socket.DatagramPacket; + +import java.net.InetSocketAddress; + +/** + * Used to allocate datagram packets that use UDP_SEGMENT (GSO). + */ +@FunctionalInterface +public interface SegmentedDatagramPacketAllocator { + + /** + * {@link SegmentedDatagramPacketAllocator} which should be used if no UDP_SEGMENT is supported and used. + */ + SegmentedDatagramPacketAllocator NONE = new SegmentedDatagramPacketAllocator() { + @Override + public int maxNumSegments() { + return 0; + } + + @Override + public DatagramPacket newPacket(ByteBuf buffer, int segmentSize, InetSocketAddress remoteAddress) { + throw new UnsupportedOperationException(); + } + }; + + /** + * The maximum number of segments to use per packet. By default this is {@code 10} but this may be overridden by + * the implementation of the interface. + * + * @return the segments. + */ + default int maxNumSegments() { + return 10; + } + + /** + * Return a new segmented {@link DatagramPacket}. + * + * @param buffer the {@link ByteBuf} that is used as content. + * @param segmentSize the size of each segment. + * @param remoteAddress the remote address to send to. + * @return the packet. + */ + DatagramPacket newPacket(ByteBuf buffer, int segmentSize, InetSocketAddress remoteAddress); +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SockaddrIn.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SockaddrIn.java new file mode 100644 index 0000000..9025c95 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SockaddrIn.java @@ -0,0 +1,187 @@ +/* + * Copyright 2020 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.handler.codec.quic; + +import io.netty.util.internal.PlatformDependent; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +final class SockaddrIn { + static final byte[] IPV4_MAPPED_IPV6_PREFIX = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xff, (byte) 0xff }; + static final int IPV4_ADDRESS_LENGTH = 4; + static final int IPV6_ADDRESS_LENGTH = 16; + static final byte[] SOCKADDR_IN6_EMPTY_ARRAY = new byte[Quiche.SIZEOF_SOCKADDR_IN6]; + static final byte[] SOCKADDR_IN_EMPTY_ARRAY = new byte[Quiche.SIZEOF_SOCKADDR_IN]; + + private SockaddrIn() { } + + static int cmp(long memory, long memory2) { + return Quiche.sockaddr_cmp(memory, memory2); + } + + static int setAddress(ByteBuffer memory, InetSocketAddress address) { + InetAddress addr = address.getAddress(); + return setAddress(addr instanceof Inet6Address, memory, address); + } + + static int setAddress(boolean ipv6, ByteBuffer memory, InetSocketAddress address) { + if (ipv6) { + return SockaddrIn.setIPv6(memory, address.getAddress(), address.getPort()); + } else { + return SockaddrIn.setIPv4(memory, address.getAddress(), address.getPort()); + } + } + + /** + * + * struct sockaddr_in { + * sa_family_t sin_family; // address family: AF_INET + * in_port_t sin_port; // port in network byte order + * struct in_addr sin_addr; // internet address + * }; + * + * // Internet address. + * struct in_addr { + * uint32_t s_addr; // address in network byte order + * }; + * + */ + static int setIPv4(ByteBuffer memory, InetAddress address, int port) { + int position = memory.position(); + try { + // memset + memory.put(SOCKADDR_IN_EMPTY_ARRAY); + + memory.putShort(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_FAMILY, Quiche.AF_INET); + memory.putShort(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_PORT, (short) port); + + byte[] bytes = address.getAddress(); + int offset = 0; + if (bytes.length == IPV6_ADDRESS_LENGTH) { + // IPV6 mapped IPV4 address, we only need the last 4 bytes. + offset = IPV4_MAPPED_IPV6_PREFIX.length; + } + assert bytes.length == offset + IPV4_ADDRESS_LENGTH; + memory.position(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_ADDR + Quiche.IN_ADDRESS_OFFSETOF_S_ADDR); + memory.put(bytes, offset, IPV4_ADDRESS_LENGTH); + return Quiche.SIZEOF_SOCKADDR_IN; + } finally { + memory.position(position); + } + } + + /** + * struct sockaddr_in6 { + * sa_family_t sin6_family; // AF_INET6 + * in_port_t sin6_port; // port number + * uint32_t sin6_flowinfo; // IPv6 flow information + * struct in6_addr sin6_addr; // IPv6 address + * uint32_t sin6_scope_id; /* Scope ID (new in 2.4) + * }; + * + * struct in6_addr { + * unsigned char s6_addr[16]; // IPv6 address + * }; + */ + static int setIPv6(ByteBuffer memory, InetAddress address, int port) { + int position = memory.position(); + try { + // memset + memory.put(SOCKADDR_IN6_EMPTY_ARRAY); + + memory.putShort(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_FAMILY, Quiche.AF_INET6); + memory.putShort(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_PORT, (short) port); + + // Skip sin6_flowinfo as we did memset before + byte[] bytes = address.getAddress(); + int offset = Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_ADDR + Quiche.IN6_ADDRESS_OFFSETOF_S6_ADDR; + + if (bytes.length == IPV4_ADDRESS_LENGTH) { + memory.position(position + offset); + memory.put(IPV4_MAPPED_IPV6_PREFIX); + + memory.position(position + offset + IPV4_MAPPED_IPV6_PREFIX.length); + memory.put(bytes, 0, IPV4_ADDRESS_LENGTH); + + // Skip sin6_scope_id as we did memset before + } else { + memory.position(position + offset); + memory.put(bytes, 0, IPV6_ADDRESS_LENGTH); + + memory.putInt(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_SCOPE_ID, + ((Inet6Address) address).getScopeId()); + } + return Quiche.SIZEOF_SOCKADDR_IN6; + } finally { + memory.position(position); + } + } + + static InetSocketAddress getIPv4(ByteBuffer memory, byte[] tmpArray) { + assert tmpArray.length == IPV4_ADDRESS_LENGTH; + int position = memory.position(); + + try { + int port = memory.getShort(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_PORT) & 0xFFFF; + memory.position(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_ADDR + Quiche.IN_ADDRESS_OFFSETOF_S_ADDR); + memory.get(tmpArray); + try { + return new InetSocketAddress(InetAddress.getByAddress(tmpArray), port); + } catch (UnknownHostException ignore) { + return null; + } + } finally { + memory.position(position); + } + } + + static InetSocketAddress getIPv6(ByteBuffer memory, byte[] ipv6Array, byte[] ipv4Array) { + assert ipv6Array.length == IPV6_ADDRESS_LENGTH; + assert ipv4Array.length == IPV4_ADDRESS_LENGTH; + int position = memory.position(); + + try { + int port = memory.getShort( + position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_PORT) & 0xFFFF; + memory.position(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_ADDR + Quiche.IN6_ADDRESS_OFFSETOF_S6_ADDR); + memory.get(ipv6Array); + if (PlatformDependent.equals( + ipv6Array, 0, IPV4_MAPPED_IPV6_PREFIX, 0, IPV4_MAPPED_IPV6_PREFIX.length)) { + System.arraycopy(ipv6Array, IPV4_MAPPED_IPV6_PREFIX.length, ipv4Array, 0, IPV4_ADDRESS_LENGTH); + try { + return new InetSocketAddress(Inet4Address.getByAddress(ipv4Array), port); + } catch (UnknownHostException ignore) { + return null; + } + } else { + int scopeId = memory.getInt(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_SCOPE_ID); + try { + return new InetSocketAddress(Inet6Address.getByAddress(null, ipv6Array, scopeId), port); + } catch (UnknownHostException ignore) { + return null; + } + } + } finally { + memory.position(position); + } + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SslEarlyDataReadyEvent.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SslEarlyDataReadyEvent.java new file mode 100644 index 0000000..be76406 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SslEarlyDataReadyEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 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.handler.codec.quic; + + +/** + * Event which is fired once it's possible to send early data on the client-side. + * See RFC8446 4.2.10 Early Data Indication. + *

+ * Users might call {@link io.netty.channel.Channel#writeAndFlush(Object)} or + * {@link io.netty.channel.ChannelHandlerContext#writeAndFlush(Object)} to send early data. + * Please be aware that early data may be replay-able and so may have other security concerns then other data. + */ +public final class SslEarlyDataReadyEvent { + + static final SslEarlyDataReadyEvent INSTANCE = new SslEarlyDataReadyEvent(); + + private SslEarlyDataReadyEvent() { } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SslSessionTicketKey.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SslSessionTicketKey.java new file mode 100644 index 0000000..ce1a42d --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/SslSessionTicketKey.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023 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: + * + * http://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.handler.codec.quic; + +import java.util.Arrays; + +/** + * Session Ticket Key + */ +public final class SslSessionTicketKey { + /** + * Size of session ticket key name + */ + public static final int NAME_SIZE = 16; + /** + * Size of session ticket key HMAC key + */ + public static final int HMAC_KEY_SIZE = 16; + /** + * Size of session ticket key AES key + */ + public static final int AES_KEY_SIZE = 16; + /** + * Size of session ticket key + */ + public static final int TICKET_KEY_SIZE = NAME_SIZE + HMAC_KEY_SIZE + AES_KEY_SIZE; + + // package private so we can access these in BoringSSLSessionTicketCallback without calling clone() on the byte[]. + final byte[] name; + final byte[] hmacKey; + final byte[] aesKey; + + /** + * Construct SessionTicketKey. + * @param name the name of the session ticket key + * @param hmacKey the HMAC key of the session ticket key + * @param aesKey the AES key of the session ticket key + */ + public SslSessionTicketKey(byte[] name, byte[] hmacKey, byte[] aesKey) { + if (name == null || name.length != NAME_SIZE) { + throw new IllegalArgumentException("Length of name must be " + NAME_SIZE); + } + if (hmacKey == null || hmacKey.length != HMAC_KEY_SIZE) { + throw new IllegalArgumentException("Length of hmacKey must be " + HMAC_KEY_SIZE); + } + if (aesKey == null || aesKey.length != AES_KEY_SIZE) { + throw new IllegalArgumentException("Length of aesKey must be " + AES_KEY_SIZE); + } + this.name = name.clone(); + this.hmacKey = hmacKey.clone(); + this.aesKey = aesKey.clone(); + } + + /** + * Get name. + * + * @return the name of the session ticket key + */ + public byte[] name() { + return name.clone(); + } + + /** + * Get HMAC key. + * @return the HMAC key of the session ticket key + */ + public byte[] hmacKey() { + return hmacKey.clone(); + } + + /** + * Get AES Key. + * @return the AES key of the session ticket key + */ + public byte[] aesKey() { + return aesKey.clone(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SslSessionTicketKey that = (SslSessionTicketKey) o; + + if (!Arrays.equals(name, that.name)) { + return false; + } + if (!Arrays.equals(hmacKey, that.hmacKey)) { + return false; + } + return Arrays.equals(aesKey, that.aesKey); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(name); + result = 31 * result + Arrays.hashCode(hmacKey); + result = 31 * result + Arrays.hashCode(aesKey); + return result; + } + + @Override + public String toString() { + return "SessionTicketKey{" + + "name=" + Arrays.toString(name) + + ", hmacKey=" + Arrays.toString(hmacKey) + + ", aesKey=" + Arrays.toString(aesKey) + + '}'; + } +} diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/package-info.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/package-info.java new file mode 100644 index 0000000..07a45ed --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 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. + */ +/** + * QUIC implementation + */ +package io.netty.handler.codec.quic; diff --git a/netty-handler-codec-quic/src/main/java/module-info.java b/netty-handler-codec-quic/src/main/java/module-info.java new file mode 100644 index 0000000..3a84174 --- /dev/null +++ b/netty-handler-codec-quic/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module org.xbib.io.netty.handler.codec.quic { + exports io.netty.handler.codec.quic; + requires org.xbib.io.netty.buffer; + requires org.xbib.io.netty.channel; + requires org.xbib.io.netty.channel.epoll; + requires org.xbib.io.netty.channel.unix; + requires org.xbib.io.netty.util; + requires org.xbib.io.netty.handler.ssl; +} diff --git a/netty-handler-codec-quic/src/main/resources/META-INF/native/libnetty_quiche_linux_x86_64.so b/netty-handler-codec-quic/src/main/resources/META-INF/native/libnetty_quiche_linux_x86_64.so new file mode 100755 index 0000000..e4040e8 Binary files /dev/null and b/netty-handler-codec-quic/src/main/resources/META-INF/native/libnetty_quiche_linux_x86_64.so differ diff --git a/netty-util/src/main/java/module-info.java b/netty-util/src/main/java/module-info.java index 827e6bd..d066d4c 100644 --- a/netty-util/src/main/java/module-info.java +++ b/netty-util/src/main/java/module-info.java @@ -5,13 +5,16 @@ module org.xbib.io.netty.util { exports io.netty.util.internal to org.xbib.io.netty.buffer, org.xbib.io.netty.channel, + org.xbib.io.netty.channel.epoll, org.xbib.io.netty.channel.unix, org.xbib.io.netty.handler, org.xbib.io.netty.handler.codec, org.xbib.io.netty.handler.codec.compression, org.xbib.io.netty.handler.codec.http, org.xbib.io.netty.handler.codec.httptwo, + org.xbib.io.netty.handler.codec.httpthree, org.xbib.io.netty.handler.codec.protobuf, + org.xbib.io.netty.handler.codec.quic, org.xbib.io.netty.handler.codec.rtsp, org.xbib.io.netty.handler.codec.spdy, org.xbib.io.netty.handler.ssl, @@ -19,13 +22,16 @@ module org.xbib.io.netty.util { exports io.netty.util.internal.logging to org.xbib.io.netty.buffer, org.xbib.io.netty.channel, + org.xbib.io.netty.channel.epoll, org.xbib.io.netty.channel.unix, org.xbib.io.netty.handler, org.xbib.io.netty.handler.codec, org.xbib.io.netty.handler.codec.compression, org.xbib.io.netty.handler.codec.http, org.xbib.io.netty.handler.codec.httptwo, + org.xbib.io.netty.handler.codec.httpthree, org.xbib.io.netty.handler.codec.protobuf, + org.xbib.io.netty.handler.codec.quic, org.xbib.io.netty.handler.codec.rtsp, org.xbib.io.netty.handler.codec.spdy, org.xbib.io.netty.handler.ssl, diff --git a/settings.gradle b/settings.gradle index 9b00221..4aac97e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -59,18 +59,21 @@ dependencyResolutionManagement { include 'netty-buffer' include 'netty-bzip2' include 'netty-channel' +include 'netty-channel-epoll' include 'netty-channel-unix' -include 'netty-jctools' include 'netty-handler' include 'netty-handler-codec' include 'netty-handler-codec-compression' include 'netty-handler-codec-http' include 'netty-handler-codec-http2' +include 'netty-handler-codec-http3' include 'netty-handler-codec-protobuf' +include 'netty-handler-codec-quic' include 'netty-handler-codec-rtsp' include 'netty-handler-codec-spdy' include 'netty-handler-ssl' include 'netty-internal-tcnative' +include 'netty-jctools' include 'netty-resolver' include 'netty-util' include 'netty-zlib'