promote netty-handler-codec-quic from incubator, add netty-handler-codec-http3

This commit is contained in:
Jörg Prante 2024-01-06 22:52:21 +01:00
parent f9b231c1be
commit fcd9b342e9
248 changed files with 45788 additions and 1 deletions

View file

@ -0,0 +1,4 @@
dependencies {
api project(':netty-channel')
api project(':netty-channel-unix')
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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() {
}
}

View file

@ -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;
}
}

View file

@ -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() {
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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];
}
}

View file

@ -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;
}

View file

@ -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
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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();
}
}
}

View file

@ -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() {
}
}

View file

@ -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;
}
}

View file

@ -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;

View 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;
}

View file

@ -5,6 +5,7 @@ module org.xbib.io.netty.channel {
exports io.netty.channel;
exports io.netty.channel.embedded;
exports io.netty.channel.group;
exports io.netty.channel.internal to org.xbib.io.netty.channel.epoll;
exports io.netty.channel.local;
exports io.netty.channel.nio;
exports io.netty.channel.oio;

View 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
}

View file

@ -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);
}
}

View file

@ -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() + ')';
}
}

View file

@ -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() + ')';
}
}

View file

@ -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() + ')';
}
}

View file

@ -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();
}
}
}

View file

@ -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() + ')';
}
}

View file

@ -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() + ')';
}
}

View file

@ -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() + ')';
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
};
}
}

View file

@ -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();
}

View file

@ -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)));
}
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}

View file

@ -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 {
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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 {
}

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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 {
}

View file

@ -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;
}
}
}

View file

@ -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());
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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)));
}
}

View file

@ -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));
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}
}

View file

@ -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