promote netty-handler-codec-quic from incubator, add netty-handler-codec-http3
This commit is contained in:
parent
f9b231c1be
commit
fcd9b342e9
248 changed files with 45788 additions and 1 deletions
4
netty-channel-epoll/build.gradle
Normal file
4
netty-channel-epoll/build.gradle
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
dependencies {
|
||||||
|
api project(':netty-channel')
|
||||||
|
api project(':netty-channel-unix')
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -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 <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> 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 <a href="https://netty.io/wiki/native-transports.html">{@code
|
||||||
|
* netty-transport-native-epoll}</a> is available.
|
||||||
|
*/
|
||||||
|
public static boolean isAvailable() {
|
||||||
|
return UNAVAILABILITY_CAUSE == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> 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 <a href="https://netty.io/wiki/native-transports.html">
|
||||||
|
* {@code netty-transport-native-epoll}</a>.
|
||||||
|
*
|
||||||
|
* @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() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelOption<?>, Object> getOptions() {
|
||||||
|
return getOptions(super.getOptions(), EpollChannelOption.EPOLL_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getOption(ChannelOption<T> 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 <T> boolean setOption(ChannelOption<T> 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}.
|
||||||
|
*
|
||||||
|
* <strong>Be aware this config setting can only be adjusted before the channel was registered.</strong>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T> extends UnixChannelOption<T> {
|
||||||
|
|
||||||
|
public static final ChannelOption<Boolean> TCP_CORK = valueOf(EpollChannelOption.class, "TCP_CORK");
|
||||||
|
public static final ChannelOption<Long> TCP_NOTSENT_LOWAT = valueOf(EpollChannelOption.class, "TCP_NOTSENT_LOWAT");
|
||||||
|
public static final ChannelOption<Integer> TCP_KEEPIDLE = valueOf(EpollChannelOption.class, "TCP_KEEPIDLE");
|
||||||
|
public static final ChannelOption<Integer> TCP_KEEPINTVL = valueOf(EpollChannelOption.class, "TCP_KEEPINTVL");
|
||||||
|
public static final ChannelOption<Integer> TCP_KEEPCNT = valueOf(EpollChannelOption.class, "TCP_KEEPCNT");
|
||||||
|
public static final ChannelOption<Integer> TCP_USER_TIMEOUT =
|
||||||
|
valueOf(EpollChannelOption.class, "TCP_USER_TIMEOUT");
|
||||||
|
public static final ChannelOption<Boolean> IP_FREEBIND = valueOf("IP_FREEBIND");
|
||||||
|
public static final ChannelOption<Boolean> IP_TRANSPARENT = valueOf("IP_TRANSPARENT");
|
||||||
|
public static final ChannelOption<Boolean> IP_RECVORIGDSTADDR = valueOf("IP_RECVORIGDSTADDR");
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ChannelOption#TCP_FASTOPEN} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static final ChannelOption<Integer> TCP_FASTOPEN = ChannelOption.TCP_FASTOPEN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link ChannelOption#TCP_FASTOPEN_CONNECT} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static final ChannelOption<Boolean> TCP_FASTOPEN_CONNECT = ChannelOption.TCP_FASTOPEN_CONNECT;
|
||||||
|
public static final ChannelOption<Integer> TCP_DEFER_ACCEPT =
|
||||||
|
ChannelOption.valueOf(EpollChannelOption.class, "TCP_DEFER_ACCEPT");
|
||||||
|
public static final ChannelOption<Boolean> TCP_QUICKACK = valueOf(EpollChannelOption.class, "TCP_QUICKACK");
|
||||||
|
public static final ChannelOption<Integer> SO_BUSY_POLL = valueOf(EpollChannelOption.class, "SO_BUSY_POLL");
|
||||||
|
|
||||||
|
public static final ChannelOption<EpollMode> EPOLL_MODE =
|
||||||
|
ChannelOption.valueOf(EpollChannelOption.class, "EPOLL_MODE");
|
||||||
|
|
||||||
|
public static final ChannelOption<Map<InetAddress, byte[]>> TCP_MD5SIG = valueOf("TCP_MD5SIG");
|
||||||
|
|
||||||
|
public static final ChannelOption<Integer> MAX_DATAGRAM_PAYLOAD_SIZE = valueOf("MAX_DATAGRAM_PAYLOAD_SIZE");
|
||||||
|
public static final ChannelOption<Boolean> UDP_GRO = valueOf("UDP_GRO");
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unused", "deprecation" })
|
||||||
|
private EpollChannelOption() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ByteBuf, InetSocketAddress> envelope =
|
||||||
|
(AddressedEnvelope<ByteBuf, InetSocketAddress>) 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<Object, SocketAddress> e = (AddressedEnvelope<Object, SocketAddress>) 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<ByteBuf, InetSocketAddress>(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelOption<?>, 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> T getOption(ChannelOption<T> 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 <T> boolean setOption(ChannelOption<T> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_TRANSPARENT</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_TRANSPARENT</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_FREEBIND</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_FREEBIND</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_RECVORIGDSTADDR</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_RECVORIGDSTADDR</a> 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 <a href="https://lwn.net/Articles/768995/">UDP_GRO</a>.
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ByteBuf, DomainSocketAddress> envelope =
|
||||||
|
(AddressedEnvelope<ByteBuf, DomainSocketAddress>) 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<Object, SocketAddress> e = (AddressedEnvelope<Object, SocketAddress>) msg;
|
||||||
|
if (e.content() instanceof ByteBuf &&
|
||||||
|
(e.recipient() == null || e.recipient() instanceof DomainSocketAddress)) {
|
||||||
|
|
||||||
|
ByteBuf content = (ByteBuf) e.content();
|
||||||
|
return UnixChannelUtil.isBufferCopyNeededForWrite(content) ?
|
||||||
|
new DefaultAddressedEnvelope<ByteBuf, DomainSocketAddress>(
|
||||||
|
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
|
||||||
|
* <a href=https://man7.org/linux/man-pages/man7/socket.7.html>SO_PEERCRED</a>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelOption<?>, Object> getOptions() {
|
||||||
|
return getOptions(
|
||||||
|
super.getOptions(),
|
||||||
|
DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, SO_SNDBUF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings({"unchecked", "deprecation"})
|
||||||
|
public <T> T getOption(ChannelOption<T> 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 <T> boolean setOption(ChannelOption<T> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <a href=https://man7.org/linux/man-pages/man7/socket.7.html>SO_PEERCRED</a>
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelOption<?>, Object> getOptions() {
|
||||||
|
return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE, ALLOW_HALF_CLOSURE, SO_SNDBUF, SO_RCVBUF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getOption(ChannelOption<T> 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 <T> boolean setOption(ChannelOption<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AbstractEpollChannel> channels = new IntObjectHashMap<AbstractEpollChannel>(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<Runnable> 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<Runnable> newTaskQueue(int maxPendingTasks) {
|
||||||
|
return newTaskQueue0(maxPendingTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
|
||||||
|
// This event loop never calls takeTask()
|
||||||
|
return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
|
||||||
|
: PlatformDependent.<Runnable>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<Channel> registeredChannelsIterator() {
|
||||||
|
assert inEventLoop();
|
||||||
|
IntObjectMap<AbstractEpollChannel> ch = channels;
|
||||||
|
if (ch.isEmpty()) {
|
||||||
|
return ChannelsReadOnlyIterator.empty();
|
||||||
|
}
|
||||||
|
return new ChannelsReadOnlyIterator<AbstractEpollChannel>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://linux.die.net//man/7/epoll">epoll</a> mode to use.
|
||||||
|
*/
|
||||||
|
public enum EpollMode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use {@code EPOLLET} (edge-triggered).
|
||||||
|
*
|
||||||
|
* @see <a href="https://linux.die.net//man/7/epoll">man 7 epoll</a>.
|
||||||
|
*/
|
||||||
|
EDGE_TRIGGERED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not use {@code EPOLLET} (level-triggered).
|
||||||
|
*
|
||||||
|
* @see <a href="https://linux.die.net//man/7/epoll">man 7 epoll</a>.
|
||||||
|
*/
|
||||||
|
LEVEL_TRIGGERED
|
||||||
|
}
|
|
@ -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 <a href="https://man7.org/linux/man-pages/man7/epoll.7.html">epoll man</a>). 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://man7.org/linux/man-pages/man7/epoll.7.html">epoll man</a>).
|
||||||
|
*
|
||||||
|
* If EPOLLRDHUP has been received we must read until we get a read error.
|
||||||
|
*/
|
||||||
|
return lastBytesRead() == attemptedBytesRead() || isReceivedRdHup();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelOption<?>, Object> getOptions() {
|
||||||
|
return getOptions(super.getOptions(), SO_RCVBUF, SO_REUSEADDR, SO_BACKLOG, TCP_FASTOPEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getOption(ChannelOption<T> 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 <T> boolean setOption(ChannelOption<T> 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 <a href="https://tools.ietf.org/html/rfc7413#appendix-A.2">RFC 7413 Passive Open</a>
|
||||||
|
*/
|
||||||
|
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 <a href="https://tools.ietf.org/html/rfc7413#appendix-A.2">RFC 7413 Passive Open</a>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<InetAddress> 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<InetAddress> tcpMd5SigAddresses() {
|
||||||
|
return tcpMd5SigAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTcpMd5Sig(Map<InetAddress, byte[]> keys) throws IOException {
|
||||||
|
// Add synchronized as newTcpMp5Sigs might do multiple operations on the socket itself.
|
||||||
|
synchronized (this) {
|
||||||
|
tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelOption<?>, Object> getOptions() {
|
||||||
|
return getOptions(super.getOptions(), EpollChannelOption.SO_REUSEPORT, EpollChannelOption.IP_FREEBIND,
|
||||||
|
EpollChannelOption.IP_TRANSPARENT, EpollChannelOption.TCP_DEFER_ACCEPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getOption(ChannelOption<T> 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 <T> boolean setOption(ChannelOption<T> 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<InetAddress, byte[]> m = (Map<InetAddress, byte[]>) 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<InetAddress, byte[]> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_FREEBIND</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_FREEBIND</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_TRANSPARENT</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_TRANSPARENT</a> 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 <a href="https://linux.die.net//man/7/tcp">TCP_DEFER_ACCEPT</a> is enabled.
|
||||||
|
*/
|
||||||
|
public int getTcpDeferAccept() {
|
||||||
|
try {
|
||||||
|
return ((EpollServerSocketChannel) channel).socket.getTcpDeferAccept();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ChannelException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<InetAddress> 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 <a href="https://linux.die.net//man/7/tcp">man 7 tcp</a>.
|
||||||
|
*/
|
||||||
|
public EpollTcpInfo tcpInfo() {
|
||||||
|
return tcpInfo(new EpollTcpInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates and returns the {@code TCP_INFO} for the current socket.
|
||||||
|
* See <a href="https://linux.die.net//man/7/tcp">man 7 tcp</a>.
|
||||||
|
*/
|
||||||
|
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<InetAddress, byte[]> keys) throws IOException {
|
||||||
|
// Add synchronized as newTcpMp5Sigs might do multiple operations on the socket itself.
|
||||||
|
synchronized (this) {
|
||||||
|
tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelOption<?>, 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> T getOption(ChannelOption<T> 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 <T> boolean setOption(ChannelOption<T> 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<InetAddress, byte[]> m = (Map<InetAddress, byte[]>) 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_TRANSPARENT</a> 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 <a href="https://man7.org/linux/man-pages/man7/ip.7.html">IP_TRANSPARENT</a> 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<InetAddress, byte[]> 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 <a href="https://linux.die.net//man/7/tcp">TCP_QUICKACK</a>
|
||||||
|
* 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 <a href="https://linux.die.net//man/7/tcp">TCP_QUICKACK</a> 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
|
||||||
|
* <a href="https://lwn.net/Articles/508865/">LWN article</a> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
* };
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<InetAddress> 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<InetAddress> 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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <p><strong>Internal usage only!</strong>
|
||||||
|
* <p>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();
|
||||||
|
/**
|
||||||
|
* <a href ="https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt">tcp_fastopen</a> client mode enabled
|
||||||
|
* state.
|
||||||
|
*/
|
||||||
|
static final boolean IS_SUPPORTING_TCP_FASTOPEN_CLIENT =
|
||||||
|
(TCP_FASTOPEN_MODE & TFO_ENABLED_CLIENT_MASK) == TFO_ENABLED_CLIENT_MASK;
|
||||||
|
/**
|
||||||
|
* <a href ="https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt">tcp_fastopen</a> 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://linux.die.net//man/2/sendmmsg">sendmmsg(...)</a> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:
|
||||||
|
* <ol>
|
||||||
|
* <li>JNI_OnLoad</li>
|
||||||
|
* <li>JNI Calls FindClass because RegisterNatives (used to register JNI methods) requires a class</li>
|
||||||
|
* <li>FindClass loads the class, but static members variables of that class attempt to call a JNI method which has not
|
||||||
|
* yet been registered.</li>
|
||||||
|
* <li>java.lang.UnsatisfiedLinkError is thrown because native method has not yet been registered.</li>
|
||||||
|
* </ol>
|
||||||
|
* 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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<InetAddress> newTcpMd5Sigs(AbstractEpollChannel channel, Collection<InetAddress> current,
|
||||||
|
Map<InetAddress, byte[]> newKeys) throws IOException {
|
||||||
|
checkNotNull(channel, "channel");
|
||||||
|
checkNotNull(current, "current");
|
||||||
|
checkNotNull(newKeys, "newKeys");
|
||||||
|
|
||||||
|
// Validate incoming values
|
||||||
|
for (Entry<InetAddress, byte[]> 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<InetAddress> addresses = new ArrayList<InetAddress>(newKeys.size());
|
||||||
|
for (Entry<InetAddress, byte[]> e : newKeys.entrySet()) {
|
||||||
|
channel.socket.setTcpMd5Sig(e.getKey(), e.getValue());
|
||||||
|
addresses.add(e.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TcpMd5Util() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <a href="https://man7.org/linux/man-pages/man7/vsock.7.html">VM sockets (Linux VSOCK address family)</a>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://en.wikipedia.org/wiki/Epoll">EPOLL Edge-Triggered Mode</a>
|
||||||
|
* for maximal performance.
|
||||||
|
*/
|
||||||
|
package io.netty.channel.epoll;
|
7
netty-channel-epoll/src/main/java/module-info.java
Normal file
7
netty-channel-epoll/src/main/java/module-info.java
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ module org.xbib.io.netty.channel {
|
||||||
exports io.netty.channel;
|
exports io.netty.channel;
|
||||||
exports io.netty.channel.embedded;
|
exports io.netty.channel.embedded;
|
||||||
exports io.netty.channel.group;
|
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.local;
|
||||||
exports io.netty.channel.nio;
|
exports io.netty.channel.nio;
|
||||||
exports io.netty.channel.oio;
|
exports io.netty.channel.oio;
|
||||||
|
|
6
netty-handler-codec-http3/build.gradle
Normal file
6
netty-handler-codec-http3/build.gradle
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
dependencies {
|
||||||
|
api project(':netty-handler-codec-http')
|
||||||
|
api project(':netty-handler-codec-quic')
|
||||||
|
testImplementation testLibs.assertj
|
||||||
|
testImplementation testLibs.mockito.core
|
||||||
|
}
|
|
@ -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<V> extends DefaultHeaders<CharSequence, V, CharSequenceMap<V>> {
|
||||||
|
CharSequenceMap() {
|
||||||
|
this(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequenceMap(boolean caseSensitive) {
|
||||||
|
this(caseSensitive, UnsupportedValueConverter.<V>instance());
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequenceMap(boolean caseSensitive, ValueConverter<V> valueConverter) {
|
||||||
|
super(caseSensitive ? CASE_SENSITIVE_HASHER : CASE_INSENSITIVE_HASHER, valueConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
CharSequenceMap(boolean caseSensitive, ValueConverter<V> valueConverter, int arraySizeHint) {
|
||||||
|
super(caseSensitive ? CASE_SENSITIVE_HASHER : CASE_INSENSITIVE_HASHER, valueConverter,
|
||||||
|
NameValidator.NOT_NULL, arraySizeHint);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + ')';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + ')';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + ')';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<CharSequence, CharSequence, Http3Headers> implements Http3Headers {
|
||||||
|
private static final ByteProcessor HTTP3_NAME_VALIDATOR_PROCESSOR = new ByteProcessor() {
|
||||||
|
@Override
|
||||||
|
public boolean process(byte value) {
|
||||||
|
return !isUpperCase(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static final NameValidator<CharSequence> HTTP3_NAME_VALIDATOR = new NameValidator<CharSequence>() {
|
||||||
|
@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<CharSequence, CharSequence> firstNonPseudo = head;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance.
|
||||||
|
* <p>
|
||||||
|
* Header names will be validated according to
|
||||||
|
* <a href="https://tools.ietf.org/html/rfc7540">rfc7540</a>.
|
||||||
|
*/
|
||||||
|
public DefaultHttp3Headers() {
|
||||||
|
this(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance.
|
||||||
|
* @param validate {@code true} to validate header names according to
|
||||||
|
* <a href="https://tools.ietf.org/html/rfc7540">rfc7540</a>. {@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
|
||||||
|
* <a href="https://tools.ietf.org/html/rfc7540">rfc7540</a>. {@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<CharSequence, CharSequence> newHeaderEntry(int h, CharSequence name, CharSequence value,
|
||||||
|
HeaderEntry<CharSequence, CharSequence> next) {
|
||||||
|
return new Http3HeaderEntry(h, name, value, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class Http3HeaderEntry extends HeaderEntry<CharSequence, CharSequence> {
|
||||||
|
protected Http3HeaderEntry(int hash, CharSequence key, CharSequence value,
|
||||||
|
HeaderEntry<CharSequence, CharSequence> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + ')';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + ')';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + ')';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Long> 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<Map.Entry<Long, Long>> 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<Long, Long> entry: settingsFrame) {
|
||||||
|
copy.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<QuicStreamChannel> HTTP3_CONTROL_STREAM_KEY =
|
||||||
|
AttributeKey.valueOf(Http3.class, "HTTP3ControlStream");
|
||||||
|
|
||||||
|
private static final AttributeKey<QpackAttributes> 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 <a
|
||||||
|
* href="https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-max_push_id">max push ID</a> received for
|
||||||
|
* this connection.
|
||||||
|
*
|
||||||
|
* @return Received <a
|
||||||
|
* href="https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-max_push_id">max push ID</a> 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<QuicStreamChannel> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2">
|
||||||
|
* Minimum number max unidirectional streams</a>.
|
||||||
|
*/
|
||||||
|
// control-stream, qpack decoder stream, qpack encoder stream
|
||||||
|
public static final int MIN_INITIAL_MAX_STREAMS_UNIDIRECTIONAL = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2">
|
||||||
|
* Minimum max data for unidirectional streams</a>.
|
||||||
|
*/
|
||||||
|
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 extends QuicCodecBuilder<T>> 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.3">CANCEL_PUSH</a>.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
|
@ -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<ChannelHandler> 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<ChannelHandler> pushStreamHandlerFactory,
|
||||||
|
LongFunction<ChannelHandler> 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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-16">
|
||||||
|
* Variable-Length Integer Encoding</a>.
|
||||||
|
*/
|
||||||
|
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 <a href="https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-16">
|
||||||
|
* Variable-Length Integer Encoding</a>.
|
||||||
|
*/
|
||||||
|
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 <a href="https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-16">
|
||||||
|
* Variable-Length Integer Encoding</a>.
|
||||||
|
*/
|
||||||
|
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 <a href="https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-16">
|
||||||
|
* Variable-Length Integer Encoding </a>
|
||||||
|
*/
|
||||||
|
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 <a href="https://tools.ietf.org/html/draft-ietf-quic-transport-32#section-16">
|
||||||
|
* Variable-Length Integer Encoding </a>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32">HTTP3</a> connections.
|
||||||
|
*/
|
||||||
|
public abstract class Http3ConnectionHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
final Http3FrameCodecFactory codecFactory;
|
||||||
|
final LongFunction<ChannelHandler> 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<ChannelHandler> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7">HTTP3 control stream</a>.
|
||||||
|
*/
|
||||||
|
public interface Http3ControlStreamFrame extends Http3Frame {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Http3ControlStreamFrame> {
|
||||||
|
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<Future<? super QuicStreamChannel>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Http3ControlStreamFrame> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.1">DATA</a>.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-8.1">HTTP3 error codes</a>.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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<Object> 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<Object> 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<Long, Long> 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 <T extends Http3Frame> void writeDynamicFrame(ChannelHandlerContext ctx, long type, T frame,
|
||||||
|
BiFunction<T, ByteBuf, Boolean> 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<Future<? super QuicStreamChannel>> {
|
||||||
|
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<? super QuicStreamChannel> 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<Future<? super QuicStreamChannel>> {
|
||||||
|
private static final Object FLUSH = new Object();
|
||||||
|
private final Deque<Object> 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<? super QuicStreamChannel> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T extends Http3Frame> extends Http3FrameTypeInboundValidationHandler<T>
|
||||||
|
implements ChannelOutboundHandler {
|
||||||
|
|
||||||
|
Http3FrameTypeDuplexValidationHandler(Class<T> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T extends Http3Frame> extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
protected final Class<T> frameType;
|
||||||
|
|
||||||
|
Http3FrameTypeInboundValidationHandler(Class<T> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T extends Http3Frame> extends ChannelOutboundHandlerAdapter {
|
||||||
|
|
||||||
|
private final Class<T> frameType;
|
||||||
|
|
||||||
|
Http3FrameTypeOutboundValidationHandler(Class<T> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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> T cast(Object msg) {
|
||||||
|
return (T) msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> boolean isValid(Class<T> 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 <T> Expected type.
|
||||||
|
* @return {@code msg} as expected frame type or {@code null} if it can not be converted to the expected type.
|
||||||
|
*/
|
||||||
|
static <T> T validateFrameWritten(Class<T> 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 <T> Expected type.
|
||||||
|
* @return {@code msg} as expected frame type or {@code null} if it can not be converted to the expected type.
|
||||||
|
*/
|
||||||
|
static <T> T validateFrameRead(Class<T> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.6">GOAWAY</a>.
|
||||||
|
*/
|
||||||
|
public interface Http3GoAwayFrame extends Http3ControlStreamFrame {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default long type() {
|
||||||
|
return Http3CodecUtils.HTTP3_GO_AWAY_FRAME_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the id.
|
||||||
|
*
|
||||||
|
* @return the id.
|
||||||
|
*/
|
||||||
|
long id();
|
||||||
|
}
|
|
@ -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<CharSequence, CharSequence, Http3Headers> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<PseudoHeaderName> PSEUDO_HEADERS = new CharSequenceMap<PseudoHeaderName>();
|
||||||
|
|
||||||
|
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<Entry<CharSequence, CharSequence>> 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<CharSequence> 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.
|
||||||
|
* <p>
|
||||||
|
* 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);
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.2">HEADERS</a>.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
|
@ -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<CharSequence, CharSequence> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.7">MAX_PUSH_ID</a>.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.5">PUSH_PROMISE</a>.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
|
@ -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<QuicStreamChannel> {
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
|
@ -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<Http3RequestStreamFrame> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7">HTTP3 push stream</a>.
|
||||||
|
*/
|
||||||
|
public interface Http3PushStreamFrame extends Http3Frame {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<QuicStreamChannel> {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-4.1">HTTP Message Exchanges</a>.
|
||||||
|
*/
|
||||||
|
final class Http3PushStreamServerValidationHandler
|
||||||
|
extends Http3FrameTypeOutboundValidationHandler<Http3PushStreamFrame> {
|
||||||
|
|
||||||
|
static final Http3PushStreamServerValidationHandler INSTANCE = new Http3PushStreamServerValidationHandler();
|
||||||
|
|
||||||
|
private Http3PushStreamServerValidationHandler() {
|
||||||
|
super(Http3PushStreamFrame.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSharable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a
|
||||||
|
* href="https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-http-message-exchanges">
|
||||||
|
* HTTP message exchange semantics</a>
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:
|
||||||
|
* <ul>
|
||||||
|
* <li>Modified {@link State} if the state should be changed.</li>
|
||||||
|
* <li>Same {@link State} as the passed {@code state} if no state change is necessary</li>
|
||||||
|
* <li>{@code null} if the frame is unexpected</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7">HTTP3 request stream</a>.
|
||||||
|
*/
|
||||||
|
public interface Http3RequestStreamFrame extends Http3Frame {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7">HTTP3 request streams</a>.
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<QuicStreamChannel> {
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
|
@ -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<Http3RequestStreamFrame> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32">HTTP3</a> 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<ChannelHandler> 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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-push-streams">push streams</a>
|
||||||
|
* 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<Http3ServerPushStreamManager> 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<Long, Object> 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<QuicStreamChannel> newPushStream(long pushId, ChannelHandler handler) {
|
||||||
|
final Promise<QuicStreamChannel> 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<QuicStreamChannel> 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<QuicStreamChannelBootstrap> bootstrapConfigurator,
|
||||||
|
Promise<QuicStreamChannel> 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<QuicStreamChannel> future,
|
||||||
|
QuicChannel channel) {
|
||||||
|
if (future.isDone()) {
|
||||||
|
sendCancelPushIfFailed(future, pushId, channel);
|
||||||
|
} else {
|
||||||
|
future.addListener(f -> sendCancelPushIfFailed(future, pushId, channel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendCancelPushIfFailed(Future<QuicStreamChannel> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.4">SETTINGS</a>.
|
||||||
|
*/
|
||||||
|
public interface Http3SettingsFrame extends Http3ControlStreamFrame, Iterable<Map.Entry<Long, Long>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See <a href="https://tools.ietf.org/html/draft-ietf-quic-qpack-19#section-5">
|
||||||
|
* SETTINGS_QPACK_MAX_TABLE_CAPACITY</a>.
|
||||||
|
*/
|
||||||
|
long HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0x1;
|
||||||
|
/**
|
||||||
|
* See <a href="https://tools.ietf.org/html/draft-ietf-quic-qpack-19#section-5">
|
||||||
|
* SETTINGS_QPACK_BLOCKED_STREAMS</a>.
|
||||||
|
*/
|
||||||
|
long HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS = 0x7;
|
||||||
|
/**
|
||||||
|
* See <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.4.1">
|
||||||
|
* SETTINGS_MAX_FIELD_SECTION_SIZE</a>.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
|
@ -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<ChannelHandler> pushStreamHandlerFactory;
|
||||||
|
|
||||||
|
Http3UnidirectionalStreamInboundClientHandler(
|
||||||
|
Http3FrameCodecFactory codecFactory,
|
||||||
|
Http3ControlStreamInboundHandler localControlStreamHandler,
|
||||||
|
Http3ControlStreamOutboundHandler remoteControlStreamHandler,
|
||||||
|
LongFunction<ChannelHandler> unknownStreamHandlerFactory,
|
||||||
|
LongFunction<ChannelHandler> pushStreamHandlerFactory,
|
||||||
|
Supplier<ChannelHandler> qpackEncoderHandlerFactory, Supplier<ChannelHandler> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Boolean> REMOTE_CONTROL_STREAM = AttributeKey.valueOf("H3_REMOTE_CONTROL_STREAM");
|
||||||
|
private static final AttributeKey<Boolean> REMOTE_QPACK_DECODER_STREAM =
|
||||||
|
AttributeKey.valueOf("H3_REMOTE_QPACK_DECODER_STREAM");
|
||||||
|
private static final AttributeKey<Boolean> REMOTE_QPACK_ENCODER_STREAM =
|
||||||
|
AttributeKey.valueOf("H3_REMOTE_QPACK_ENCODER_STREAM");
|
||||||
|
|
||||||
|
final Http3FrameCodecFactory codecFactory;
|
||||||
|
final Http3ControlStreamInboundHandler localControlStreamHandler;
|
||||||
|
final Http3ControlStreamOutboundHandler remoteControlStreamHandler;
|
||||||
|
final Supplier<ChannelHandler> qpackEncoderHandlerFactory;
|
||||||
|
final Supplier<ChannelHandler> qpackDecoderHandlerFactory;
|
||||||
|
final LongFunction<ChannelHandler> unknownStreamHandlerFactory;
|
||||||
|
|
||||||
|
Http3UnidirectionalStreamInboundHandler(Http3FrameCodecFactory codecFactory,
|
||||||
|
Http3ControlStreamInboundHandler localControlStreamHandler,
|
||||||
|
Http3ControlStreamOutboundHandler remoteControlStreamHandler,
|
||||||
|
LongFunction<ChannelHandler> unknownStreamHandlerFactory,
|
||||||
|
Supplier<ChannelHandler> qpackEncoderHandlerFactory,
|
||||||
|
Supplier<ChannelHandler> 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<Object> 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
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.1">control stream</a>.
|
||||||
|
*/
|
||||||
|
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<Boolean> key) {
|
||||||
|
return ctx.channel().parent().attr(key).setIfAbsent(true) == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called if the current {@link Channel} is a
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-6.2.2">push stream</a>.
|
||||||
|
*/
|
||||||
|
abstract void initPushStream(ChannelHandlerContext ctx, long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called if the current {@link Channel} is a
|
||||||
|
* <a href="https://www.ietf.org/archive/id/draft-ietf-quic-qpack-19.html#name-encoder-and-decoder-streams">
|
||||||
|
* QPACK encoder stream</a>.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
* <a href="https://www.ietf.org/archive/id/draft-ietf-quic-qpack-19.html#name-encoder-and-decoder-streams">
|
||||||
|
* QPACK decoder stream</a>.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ChannelHandler> unknownStreamHandlerFactory,
|
||||||
|
Supplier<ChannelHandler> qpackEncoderHandlerFactory,
|
||||||
|
Supplier<ChannelHandler> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8">Unknown HTTP3 frame</a>.
|
||||||
|
* These frames are valid on all stream types.
|
||||||
|
* <pre>
|
||||||
|
* HTTP/3 Frame Format {
|
||||||
|
* Type (i),
|
||||||
|
* Length (i),
|
||||||
|
* Frame Payload (..),
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
|
@ -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<AsciiString> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">[RFC 7540], 8.1.2.3</a> 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}
|
||||||
|
* <p>
|
||||||
|
* {@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}
|
||||||
|
* <p>
|
||||||
|
* {@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}
|
||||||
|
* <p>
|
||||||
|
* {@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
|
||||||
|
* <p>
|
||||||
|
* {@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 <ul>
|
||||||
|
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||||
|
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||||
|
* </ul>
|
||||||
|
* @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 <ul>
|
||||||
|
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||||
|
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||||
|
* </ul>
|
||||||
|
* @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 <ul>
|
||||||
|
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||||
|
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||||
|
* </ul>
|
||||||
|
* @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 <ul>
|
||||||
|
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||||
|
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||||
|
* </ul>
|
||||||
|
* @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 <a href="https://tools.ietf.org/html/rfc7230">rfc7230</a>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link ExtensionHeaderNames#SCHEME}</li>
|
||||||
|
* </ul>
|
||||||
|
* {@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<AsciiString> toLowercaseMap(Iterator<? extends CharSequence> valuesIter,
|
||||||
|
int arraySizeHint) {
|
||||||
|
UnsupportedValueConverter<AsciiString> valueConverter = UnsupportedValueConverter.<AsciiString>instance();
|
||||||
|
CharSequenceMap<AsciiString> result = new CharSequenceMap<AsciiString>(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
|
||||||
|
* <a href="https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1">
|
||||||
|
* special rules in the HTTP/3 RFC</a>.
|
||||||
|
* @param entry An entry whose name is {@link HttpHeaderNames#TE}.
|
||||||
|
* @param out the resulting HTTP/3 headers.
|
||||||
|
*/
|
||||||
|
private static void toHttp3HeadersFilterTE(Entry<CharSequence, CharSequence> entry,
|
||||||
|
Http3Headers out) {
|
||||||
|
if (indexOf(entry.getValue(), ',', 0) == -1) {
|
||||||
|
if (contentEqualsIgnoreCase(trim(entry.getValue()), TRAILERS)) {
|
||||||
|
out.add(TE, TRAILERS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<CharSequence> 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<Entry<CharSequence, CharSequence>> 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<AsciiString> connectionBlacklist =
|
||||||
|
toLowercaseMap(inHeaders.valueCharSequenceIterator(CONNECTION), 8);
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
Entry<CharSequence, CharSequence> 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
|
||||||
|
* <a href="https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1">HTTP3 spec</a>.
|
||||||
|
*/
|
||||||
|
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<AsciiString>
|
||||||
|
REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
|
||||||
|
private static final CharSequenceMap<AsciiString>
|
||||||
|
RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
|
||||||
|
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<AsciiString> 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<Entry<CharSequence, CharSequence>> inputHeaders) throws Http3Exception {
|
||||||
|
// lazily created as needed
|
||||||
|
StringBuilder cookies = null;
|
||||||
|
|
||||||
|
for (Entry<CharSequence, CharSequence> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<QuicStreamChannel> encoderStreamPromise;
|
||||||
|
private final Promise<QuicStreamChannel> 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<Future<? super QuicStreamChannel>> listener) {
|
||||||
|
assert !dynamicTableDisabled;
|
||||||
|
assert encoderStreamPromise != null;
|
||||||
|
encoderStreamPromise.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void whenDecoderStreamAvailable(GenericFutureListener<Future<? super QuicStreamChannel>> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<List<Runnable>> 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 <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-blocked-streams">blocked streams</a>.
|
||||||
|
* 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<CharSequence, CharSequence> 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
|
||||||
|
* <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-set-dynamic-table-capacity">
|
||||||
|
* encoder instruction.</a>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-name-reference">
|
||||||
|
* encoder instruction.</a>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-literal-name">
|
||||||
|
* encoder instruction.</a>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-literal-name">
|
||||||
|
* encoder instruction.</a>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-abandonment-of-a-stream"> abandoned</a>
|
||||||
|
*
|
||||||
|
* @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<CharSequence, CharSequence> 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<CharSequence, CharSequence> 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<CharSequence, CharSequence> 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<CharSequence, CharSequence> 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<CharSequence, CharSequence> 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<Runnable> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue