Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UNDERTOW-1938][UNDERTOW-2259][UNDERTOW-2258][UNDERTOW-2246][UNDERTOW-2251] Backport bug fixes #1469

Merged
merged 9 commits into from
Apr 18, 2023
Merged
189 changes: 114 additions & 75 deletions core/src/main/java/io/undertow/client/http2/Http2ClientConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,65 +18,67 @@

package io.undertow.client.http2;

import static io.undertow.protocols.http2.Http2Channel.AUTHORITY;
import static io.undertow.protocols.http2.Http2Channel.METHOD;
import static io.undertow.protocols.http2.Http2Channel.PATH;
import static io.undertow.protocols.http2.Http2Channel.SCHEME;
import static io.undertow.protocols.http2.Http2Channel.STATUS;
import static io.undertow.util.Headers.CONTENT_LENGTH;
import static io.undertow.util.Headers.TRANSFER_ENCODING;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientConnection;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientRequest;
import io.undertow.client.ClientStatistics;
import io.undertow.connector.ByteBufferPool;
import io.undertow.protocols.http2.AbstractHttp2StreamSourceChannel;
import io.undertow.protocols.http2.Http2Channel;
import io.undertow.protocols.http2.Http2DataStreamSinkChannel;
import io.undertow.protocols.http2.Http2GoAwayStreamSourceChannel;
import io.undertow.protocols.http2.Http2HeadersStreamSinkChannel;
import io.undertow.protocols.http2.Http2PingStreamSourceChannel;
import io.undertow.protocols.http2.Http2PushPromiseStreamSourceChannel;
import io.undertow.protocols.http2.Http2RstStreamStreamSourceChannel;
import io.undertow.protocols.http2.Http2StreamSourceChannel;
import io.undertow.server.protocol.http.HttpAttachments;
import io.undertow.util.HeaderMap;
import io.undertow.util.HeaderValues;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import io.undertow.util.Protocols;
import org.xnio.ChannelExceptionHandler;
import org.xnio.ChannelListener;
import org.xnio.ChannelListeners;
import org.xnio.IoUtils;
import org.xnio.Option;
import io.undertow.connector.ByteBufferPool;
import org.xnio.StreamConnection;
import org.xnio.XnioIoThread;
import org.xnio.XnioWorker;
import org.xnio.channels.Channels;
import org.xnio.channels.StreamSinkChannel;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientConnection;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientRequest;
import io.undertow.protocols.http2.AbstractHttp2StreamSourceChannel;
import io.undertow.protocols.http2.Http2Channel;
import io.undertow.protocols.http2.Http2HeadersStreamSinkChannel;
import io.undertow.protocols.http2.Http2PingStreamSourceChannel;
import io.undertow.protocols.http2.Http2RstStreamStreamSourceChannel;
import io.undertow.protocols.http2.Http2StreamSourceChannel;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

import static io.undertow.protocols.http2.Http2Channel.AUTHORITY;
import static io.undertow.protocols.http2.Http2Channel.METHOD;
import static io.undertow.protocols.http2.Http2Channel.PATH;
import static io.undertow.protocols.http2.Http2Channel.SCHEME;
import static io.undertow.protocols.http2.Http2Channel.STATUS;
import static io.undertow.util.Headers.CONTENT_LENGTH;
import static io.undertow.util.Headers.TRANSFER_ENCODING;

/**
* ClientConnection implementation for HTTP2 protocol.
*
* @author Stuart Douglas
* @author Flavia Rainone
*/
public class Http2ClientConnection implements ClientConnection {

Expand Down Expand Up @@ -386,6 +388,49 @@ public void sendPing(PingListener listener, long timeout, TimeUnit timeUnit) {

private class Http2ReceiveListener implements ChannelListener<Http2Channel> {

// listener that handles events for channels after receiving a continue response
private class ContinueReceiveListener implements ChannelListener<AbstractHttp2StreamSourceChannel> {
private final Http2Channel http2Channel;

ContinueReceiveListener(Http2Channel http2Channel) {
this.http2Channel = http2Channel;
}

@Override
public void handleEvent(AbstractHttp2StreamSourceChannel sourceChannel) {
// listener is added only to instances of Http2StreamSourceChannel
assert sourceChannel instanceof Http2StreamSourceChannel;
try {
// channel is already created, no need to invoke receive
final Http2StreamSourceChannel channel = (Http2StreamSourceChannel) sourceChannel;
if (channel.getHeaders().getFirst(STATUS) == null) {
// instead, process pending frames, so we can see if we have a final status
Channels.drain(channel, Long.MAX_VALUE);
if (channel.getHeaders().getFirst(STATUS) == null) {
// no status yet, return and wait for next event
return;
}
}
// finally, a new status
int statusCode = Integer.parseInt(channel.getHeaders().getFirst(STATUS));
Http2ClientExchange request = currentExchanges.get(channel.getStreamId());
if (statusCode < 200) {
//this is an informational response 1xx response
if (statusCode == 100) {
//we got a continue response again, just set the continue response and wait for next event
request.setContinueResponse(request.createResponse(channel));
}
Channels.drain(channel, Long.MAX_VALUE);
return;
}
// we got the final response, handle it
handleFinalResponse(http2Channel, request, channel);
} catch (Throwable t) {
handleThrowable(t);
}
}
}

@Override
public void handleEvent(Http2Channel channel) {
try {
Expand All @@ -400,39 +445,15 @@ public void handleEvent(Http2Channel channel) {
if(statusCode == 100) {
//a continue response
request.setContinueResponse(request.createResponse(streamSourceChannel));
// switch to continue receive listener, because next frame we will already have the Http2StreamSourceChannel
// previously created, we just need to read the new pending frames as they arrive
streamSourceChannel.getReadSetter().set(new ContinueReceiveListener(http2Channel));
streamSourceChannel.resumeReads();
}
Channels.drain(result, Long.MAX_VALUE);
return;
}
((Http2StreamSourceChannel) result).setTrailersHandler(new Http2StreamSourceChannel.TrailersHandler() {
@Override
public void handleTrailers(HeaderMap headerMap) {
request.putAttachment(HttpAttachments.REQUEST_TRAILERS, headerMap);
}
});

result.addCloseTask(new ChannelListener<AbstractHttp2StreamSourceChannel>() {
@Override
public void handleEvent(AbstractHttp2StreamSourceChannel channel) {
currentExchanges.remove(streamSourceChannel.getStreamId());
}
});
streamSourceChannel.setCompletionListener(new ChannelListener<Http2StreamSourceChannel>() {
@Override
public void handleEvent(Http2StreamSourceChannel channel) {
currentExchanges.remove(streamSourceChannel.getStreamId());
}
});
if (request == null && initialUpgradeRequest) {
Channels.drain(result, Long.MAX_VALUE);
initialUpgradeRequest = false;
return;
} else if(request == null) {
channel.sendGoAway(Http2Channel.ERROR_PROTOCOL_ERROR);
IoUtils.safeClose(Http2ClientConnection.this);
return;
}
request.responseReady(streamSourceChannel);
handleFinalResponse(channel, request, streamSourceChannel);
} else if (result instanceof Http2PingStreamSourceChannel) {
handlePing((Http2PingStreamSourceChannel) result);
} else if (result instanceof Http2RstStreamStreamSourceChannel) {
Expand Down Expand Up @@ -485,18 +506,24 @@ public void handleEvent(Http2StreamSourceChannel channel) {
}

} catch (Throwable t) {
IOException e = t instanceof IOException ? (IOException) t : new IOException(t);
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
IoUtils.safeClose(Http2ClientConnection.this);
for (Map.Entry<Integer, Http2ClientExchange> entry : currentExchanges.entrySet()) {
try {
entry.getValue().failed(e);
} catch (Throwable ex) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(ex));
}
}
handleThrowable(t);
}
}

private void handleFinalResponse(Http2Channel channel, Http2ClientExchange request, Http2StreamSourceChannel response) throws IOException {
response.setTrailersHandler(headerMap -> request.putAttachment(io.undertow.server.protocol.http.HttpAttachments.REQUEST_TRAILERS, headerMap));
response.addCloseTask(channel1 -> currentExchanges.remove(response.getStreamId()));
response.setCompletionListener(channel12 -> currentExchanges.remove(response.getStreamId()));
if (request == null && initialUpgradeRequest) {
Channels.drain(response, Long.MAX_VALUE);
initialUpgradeRequest = false;
return;
} else if(request == null) {
channel.sendGoAway(io.undertow.protocols.http2.Http2Channel.ERROR_PROTOCOL_ERROR);
IoUtils.safeClose(Http2ClientConnection.this);
return;
}
request.responseReady(response);
}

private void handlePing(Http2PingStreamSourceChannel frame) {
Expand All @@ -512,6 +539,18 @@ private void handlePing(Http2PingStreamSourceChannel frame) {
}
}

private void handleThrowable(Throwable t) {
final IOException e = t instanceof IOException ? (IOException) t : new IOException(t);
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
IoUtils.safeClose(Http2ClientConnection.this);
for (Map.Entry<Integer, Http2ClientExchange> entry : currentExchanges.entrySet()) {
try {
entry.getValue().failed(e);
} catch (Throwable ex) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(ex));
}
}
}
}

private static final class PingKey{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ protected SendFrameHeader createFrameHeaderImpl() {
firstBuffer.put(0, (byte) ((headerFrameLength >> 16) & 0xFF));
firstBuffer.put(1, (byte) ((headerFrameLength >> 8) & 0xFF));
firstBuffer.put(2, (byte) (headerFrameLength & 0xFF));
firstBuffer.put(4, (byte) ((isFinalFrameQueued() && !getBuffer().hasRemaining() && frameType == Http2Channel.FRAME_TYPE_HEADERS && trailers == null ? Http2Channel.HEADERS_FLAG_END_STREAM : 0) | (result == HpackEncoder.State.COMPLETE ? Http2Channel.HEADERS_FLAG_END_HEADERS : 0 ) | (paddingBytes > 0 ? Http2Channel.HEADERS_FLAG_PADDED : 0))); //flags
firstBuffer.put(4, (byte) ((isFinalFrameQueued() && !getBuffer().hasRemaining() && frameType == Http2Channel.FRAME_TYPE_HEADERS && !isContinueStatus() && trailers == null ? Http2Channel.HEADERS_FLAG_END_STREAM : 0) | (result == HpackEncoder.State.COMPLETE ? Http2Channel.HEADERS_FLAG_END_HEADERS : 0 ) | (paddingBytes > 0 ? Http2Channel.HEADERS_FLAG_PADDED : 0))); //flags
ByteBuffer currentBuffer = firstBuffer;

if(currentBuffer.remaining() < paddingBytes) {
Expand Down Expand Up @@ -265,6 +265,10 @@ protected SendFrameHeader createFrameHeaderImpl() {

}

private boolean isContinueStatus() {
return "100".equals(this.getHeaders().getFirst(Http2Channel.STATUS));
}

private HpackEncoder.State encodeContinuationFrame(HeaderMap headers, PooledByteBuffer current) {
ByteBuffer currentBuffer;
HpackEncoder.State result;//continuation frame
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@

package io.undertow.protocols.http2;

import io.undertow.UndertowMessages;
import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel;
import io.undertow.server.protocol.framed.FrameHeaderData;

import java.io.IOException;
import java.nio.ByteBuffer;

import static io.undertow.protocols.http2.Http2Channel.DATA_FLAG_END_STREAM;
import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_CONTINUATION;
import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_DATA;
Expand All @@ -30,17 +37,9 @@
import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_WINDOW_UPDATE;
import static io.undertow.protocols.http2.Http2Channel.HEADERS_FLAG_END_HEADERS;
import static org.xnio.Bits.allAreClear;
import static org.xnio.Bits.anyAreClear;
import static org.xnio.Bits.allAreSet;
import static org.xnio.Bits.anyAreSet;

import java.io.IOException;
import java.nio.ByteBuffer;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel;
import io.undertow.server.protocol.framed.FrameHeaderData;

/**
* @author Stuart Douglas
*/
Expand Down Expand Up @@ -212,7 +211,7 @@ int getActualLength() {

@Override
public AbstractFramedStreamSourceChannel<?, ?, ?> getExistingChannel() {
Http2StreamSourceChannel http2StreamSourceChannel;
final Http2StreamSourceChannel http2StreamSourceChannel;
if (type == FRAME_TYPE_DATA ||
type == Http2Channel.FRAME_TYPE_CONTINUATION ||
type == Http2Channel.FRAME_TYPE_PRIORITY ) {
Expand All @@ -232,16 +231,9 @@ int getActualLength() {
}
return http2StreamSourceChannel;
} else if(type == FRAME_TYPE_HEADERS) {
//headers can actually be a trailer

Http2StreamSourceChannel channel = http2Channel.getIncomingStream(streamId);
final Http2StreamSourceChannel channel = http2Channel.getIncomingStream(streamId);
if(channel != null) {
if(anyAreClear(flags, Http2Channel.HEADERS_FLAG_END_STREAM)) {
//this is a protocol error
UndertowLogger.REQUEST_IO_LOGGER.debug("Received HTTP/2 trailers header without end stream set");
http2Channel.sendGoAway(Http2Channel.ERROR_PROTOCOL_ERROR);
}
if (!channel.isHeadersEndStream() && anyAreSet(flags, Http2Channel.HEADERS_FLAG_END_HEADERS)) {
if (!channel.isHeadersEndStream() && allAreSet(flags, Http2Channel.HEADERS_FLAG_END_HEADERS | Http2Channel.HEADERS_FLAG_END_STREAM)) {
http2Channel.removeStreamSource(streamId);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@

package io.undertow.protocols.http2;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import io.undertow.UndertowLogger;
import io.undertow.connector.PooledByteBuffer;
import io.undertow.server.protocol.framed.FrameHeaderData;
import io.undertow.util.HeaderMap;
import io.undertow.util.Headers;
import org.xnio.Bits;
import org.xnio.ChannelListener;
import org.xnio.ChannelListeners;
import io.undertow.UndertowLogger;
import io.undertow.connector.PooledByteBuffer;
import org.xnio.IoUtils;
import org.xnio.channels.StreamSinkChannel;

import io.undertow.server.protocol.framed.FrameHeaderData;
import io.undertow.util.HeaderMap;
import io.undertow.util.Headers;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
* @author Stuart Douglas
Expand Down Expand Up @@ -92,6 +92,7 @@ protected void handleHeaderData(FrameHeaderData headerData) {
}
}
} else if(parser instanceof Http2HeadersParser) {
this.headers.putAll(((Http2HeadersParser) parser).getHeaderMap());
if(trailersHandler != null) {
trailersHandler.handleTrailers(((Http2HeadersParser) parser).getHeaderMap());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ public final class DirectByteBufferDeallocator {

static {
String versionString = System.getProperty("java.specification.version");
if(versionString.startsWith("1.")) {
if(versionString.equals("0.9")) {
//android hardcoded
versionString = "11";
} else if(versionString.startsWith("1.")) {
versionString = versionString.substring(2);
}
int version = Integer.parseInt(versionString);
Expand Down