Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public enum H2Param {
MAX_CONCURRENT_STREAMS(0x3),
INITIAL_WINDOW_SIZE(0x4),
MAX_FRAME_SIZE(0x5),
MAX_HEADER_LIST_SIZE(0x6);
MAX_HEADER_LIST_SIZE(0x6),
SETTINGS_NO_RFC7540_PRIORITIES (0x9);

int code;

Expand All @@ -50,25 +51,32 @@ public int getCode() {
return code;
}

private static final H2Param[] LOOKUP_TABLE = new H2Param[6];
private static final H2Param[] LOOKUP_TABLE;
static {
for (final H2Param param: H2Param.values()) {
LOOKUP_TABLE[param.code - 1] = param;
int max = 0;
for (final H2Param p : H2Param.values()) {
if (p.code > max) {
max = p.code;
}
}
LOOKUP_TABLE = new H2Param[max + 1];
for (final H2Param p : H2Param.values()) {
LOOKUP_TABLE[p.code] = p;
}
}

public static H2Param valueOf(final int code) {
if (code < 1 || code > LOOKUP_TABLE.length) {
if (code < 0 || code >= LOOKUP_TABLE.length) {
return null;
}
return LOOKUP_TABLE[code - 1];
return LOOKUP_TABLE[code];
}

public static String toString(final int code) {
if (code < 1 || code > LOOKUP_TABLE.length) {
if (code < 0 || code >= LOOKUP_TABLE.length || LOOKUP_TABLE[code] == null) {
return Integer.toString(code);
}
return LOOKUP_TABLE[code - 1].name();
return LOOKUP_TABLE[code].name();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ public enum FrameType {
PING(0x06),
GOAWAY(0x07),
WINDOW_UPDATE(0x08),
CONTINUATION(0x09);
CONTINUATION(0x09),
PRIORITY_UPDATE(0x10); // 16

int value;
final int value;

FrameType(final int value) {
this.value = value;
Expand All @@ -54,25 +55,38 @@ public int getValue() {
return value;
}

private static final FrameType[] LOOKUP_TABLE = new FrameType[10];
private static final FrameType[] LOOKUP_TABLE;
static {
for (final FrameType frameType: FrameType.values()) {
LOOKUP_TABLE[frameType.value] = frameType;
int max = -1;
for (final FrameType t : FrameType.values()) {
if (t.value > max) {
max = t.value;
}
}
LOOKUP_TABLE = new FrameType[max + 1];
for (final FrameType t : FrameType.values()) {
LOOKUP_TABLE[t.value] = t;
}
}

public static FrameType valueOf(final int value) {
if (value < 0 || value >= LOOKUP_TABLE.length) {
return null;
}
return LOOKUP_TABLE[value];
return LOOKUP_TABLE[value]; // may be null for gaps (e.g., 0x0A..0x0F)
}

public static String toString(final int value) {
if (value < 0 || value >= LOOKUP_TABLE.length) {
return Integer.toString(value);
}
return LOOKUP_TABLE[value].name();
final FrameType t = LOOKUP_TABLE[value];
return t != null ? t.name() : Integer.toString(value);
}

/** Convenience: compare this enum to a raw frame type byte. */
public boolean same(final int rawType) {
return this.value == rawType;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.apache.hc.core5.http2.protocol.H2RequestConformance;
import org.apache.hc.core5.http2.protocol.H2RequestConnControl;
import org.apache.hc.core5.http2.protocol.H2RequestContent;
import org.apache.hc.core5.http2.protocol.H2RequestPriority;
import org.apache.hc.core5.http2.protocol.H2RequestTargetHost;
import org.apache.hc.core5.http2.protocol.H2RequestValidateHost;
import org.apache.hc.core5.http2.protocol.H2ResponseConformance;
Expand Down Expand Up @@ -86,6 +87,7 @@ public static HttpProcessorBuilder customClient(final String agentInfo) {
H2RequestTargetHost.INSTANCE,
H2RequestContent.INSTANCE,
H2RequestConnControl.INSTANCE,
H2RequestPriority.INSTANCE,
new RequestUserAgent(!TextUtils.isBlank(agentInfo) ? agentInfo :
VersionInfo.getSoftwareInfo(SOFTWARE, "org.apache.hc.core5", HttpProcessors.class)),
RequestExpectContinue.INSTANCE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -137,6 +138,10 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
private EndpointDetails endpointDetails;
private boolean goAwayReceived;

// RFC 9218 gating state
private volatile boolean serverSettingsSeen;
private volatile boolean remoteNoH2Priorities;

AbstractH2StreamMultiplexer(
final ProtocolIOSession ioSession,
final FrameFactory frameFactory,
Expand Down Expand Up @@ -173,6 +178,9 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }

this.lowMark = H2Config.INIT.getInitialWindowSize() / 2;
this.streamListener = streamListener;

this.serverSettingsSeen = false;
this.remoteNoH2Priorities = false;
}

@Override
Expand All @@ -188,7 +196,6 @@ public String getId() {

abstract H2StreamHandler createRemotelyInitiatedStream(
H2StreamChannel channel,

HttpProcessor httpProcessor,
BasicHttpConnectionMetrics connMetrics,
HandlerFactory<AsyncPushConsumer> pushHandlerFactory) throws IOException;
Expand Down Expand Up @@ -261,6 +268,11 @@ private void commitFrame(final RawFrame frame) throws IOException {
}
}

protected final void writeExtensionFrame(final int type, final int flags, final int streamId, final ByteBuffer payload)
throws IOException {
commitFrame(new RawFrame(type, flags, streamId, payload));
}

private void commitHeaders(
final int streamId, final List<? extends Header> headers, final boolean endStream) throws IOException {
if (streamListener != null) {
Expand Down Expand Up @@ -418,8 +430,9 @@ public final void onConnect() throws HttpException, IOException {
new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()),
new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()),
new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()),
new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()));

new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()),
// RFC 9218 MUST: advertise intent to ignore RFC 7540 priorities in the first SETTINGS
new H2Setting(H2Param.SETTINGS_NO_RFC7540_PRIORITIES, 1));
commitFrame(settingsFrame);
localSettingState = SettingsHandshake.TRANSMITTED;
maximizeWindow(0, connInputWindow);
Expand Down Expand Up @@ -547,10 +560,10 @@ public final void onTimeout(final Timeout timeout) throws HttpException, IOExcep
final RawFrame goAway;
if (localSettingState != SettingsHandshake.ACKED) {
goAway = frameFactory.createGoAway(processedRemoteStreamId, H2Error.SETTINGS_TIMEOUT,
"Setting timeout (" + timeout + ")");
"Setting timeout (" + timeout + ")");
} else {
goAway = frameFactory.createGoAway(processedRemoteStreamId, H2Error.NO_ERROR,
"Timeout due to inactivity (" + timeout + ")");
"Timeout due to inactivity (" + timeout + ")");
}
commitFrame(goAway);
for (final Iterator<Map.Entry<Integer, H2Stream>> it = streamMap.entrySet().iterator(); it.hasNext(); ) {
Expand Down Expand Up @@ -918,10 +931,10 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
if ((payload.remaining() % 6) != 0) {
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid SETTINGS payload");
}
consumeSettingsFrame(payload);
remoteSettingState = SettingsHandshake.TRANSMITTED;
consumeSettingsFrame(payload); // inside: set remoteNoH2Priorities if NO_RFC7540=1 seen
}
// Send ACK
serverSettingsSeen = true;
remoteSettingState = SettingsHandshake.TRANSMITTED;
final RawFrame response = frameFactory.createSettingsAck();
commitFrame(response);
remoteSettingState = SettingsHandshake.ACKED;
Expand Down Expand Up @@ -1021,9 +1034,21 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
}
ioSession.setEvent(SelectionKey.OP_WRITE);
break;
case PRIORITY_UPDATE: {
onPriorityUpdateFrame(frame);
}
break;
default:
break;
}
}

protected void onPriorityUpdateFrame(final RawFrame frame) throws H2ConnectionException {
parsePriorityUpdatePayload(frame); // allows empty PFV
// At this layer we don't need to parse the dictionary; unknown/ext params are fine.
// Apply 'u.priorityFieldValue' to your local scheduler as needed; no errors for empty.
}

private void consumeDataFrame(final RawFrame frame, final H2Stream stream) throws HttpException, IOException {
if (stream.isRemoteClosed()) {
throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed");
Expand Down Expand Up @@ -1093,7 +1118,6 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
}
final ByteBuffer payload = frame.getPayloadContent();
if (frame.isFlagSet(FrameFlag.PRIORITY)) {
// Priority not supported
payload.getInt();
payload.get();
}
Expand Down Expand Up @@ -1193,9 +1217,21 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, ex.getMessage());
}
break;
case SETTINGS_NO_RFC7540_PRIORITIES:
if (value != 0 && value != 1) {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Invalid value for SETTINGS_NO_RFC7540_PRIORITIES");
}
if (serverSettingsSeen && remoteNoH2Priorities != (value == 1)) {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "SETTINGS_NO_RFC7540_PRIORITIES changed");
}
remoteNoH2Priorities = value == 1;
break;
}
}
}
if (!serverSettingsSeen) {
serverSettingsSeen = true;
}
applyRemoteSettings(configBuilder.build());
}

Expand Down Expand Up @@ -1336,6 +1372,27 @@ void appendState(final StringBuilder buf) {
.append(", processedRemoteStreamId=").append(processedRemoteStreamId);
}

private boolean isPriorityUpdateAllowed() {
// pre-SETTINGS: allowed; post-SETTINGS: only if peer said "1"
return !serverSettingsSeen || remoteNoH2Priorities;
}

private void sendPriorityUpdateInternal(final int prioritizedStreamId,
final String priorityFieldValue) throws IOException {
if (priorityFieldValue == null) {
return;
}
if (!isPriorityUpdateAllowed()) {
return; // suppressed per RFC 9218 §2.1.1
}
final byte[] ascii = priorityFieldValue.getBytes(StandardCharsets.US_ASCII);
final ByteBuffer payload = ByteBuffer.allocate(4 + ascii.length);
payload.putInt(prioritizedStreamId & 0x7FFFFFFF);
payload.put(ascii);
payload.flip();
writeExtensionFrame(FrameType.PRIORITY_UPDATE.getValue(), 0, 0, payload);
}

private static class Continuation {

final int streamId;
Expand Down Expand Up @@ -1418,6 +1475,12 @@ public void submit(final List<Header> headers, final boolean endStream) throws I
if (localEndStream) {
return;
}
for (final Header h : headers) {
if ("priority".equalsIgnoreCase(h.getName())) {
sendPriorityUpdateInternal(id, h.getValue());
break;
}
}
idle = false;
commitHeaders(id, headers, endStream);
if (endStream) {
Expand Down Expand Up @@ -1746,4 +1809,43 @@ public String toString() {

}

}
/**
* RFC 9218 parsing helper for PRIORITY_UPDATE.
* - Validates streamId == 0 (HTTP/2 control stream)
* - Enforces payload length >= 4 (31-bit stream ID); allows exactly 4 (empty Priority Field Value)
* - Returns ASCII Priority Field Value as-is (unknown/ext parameters preserved).
*
* Base class does NOT call this by default; a server-side subclass can call it from
* {@link #onPriorityUpdateFrame(RawFrame)} to accept client PRIORITY_UPDATE frames.
*/
protected final PrioritizedUpdate parsePriorityUpdatePayload(final RawFrame frame) throws H2ConnectionException {
if (frame.getStreamId() != 0) {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "PRIORITY_UPDATE must use stream 0");
}
final ByteBuffer payload = frame.getPayloadContent();
if (payload == null || payload.remaining() < 4) {
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid PRIORITY_UPDATE payload");
}
final int prioritizedId = payload.getInt() & 0x7FFF_FFFF;

// Allow empty Priority Field Value (payload length exactly 4) => "all defaults"
final String fieldValue;
if (payload.hasRemaining()) {
// Preserve as-is (unknown/ext params are ignored by us but kept intact)
fieldValue = StandardCharsets.US_ASCII.decode(payload.slice()).toString();
} else {
fieldValue = "";
}
return new PrioritizedUpdate(prioritizedId, fieldValue);
}

protected static final class PrioritizedUpdate {
public final int prioritizedStreamId;
public final String priorityFieldValue; // may be empty => "all defaults"
PrioritizedUpdate(final int prioritizedStreamId, final String priorityFieldValue) {
this.prioritizedStreamId = prioritizedStreamId;
this.priorityFieldValue = priorityFieldValue != null ? priorityFieldValue : "";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.apache.hc.core5.http2.config.H2Config;
import org.apache.hc.core5.http2.frame.DefaultFrameFactory;
import org.apache.hc.core5.http2.frame.FrameFactory;
import org.apache.hc.core5.http2.frame.RawFrame;
import org.apache.hc.core5.http2.frame.StreamIdGenerator;
import org.apache.hc.core5.reactor.ProtocolIOSession;

Expand All @@ -51,6 +52,8 @@
* client side HTTP/2 messaging protocol with full support for
* multiplexed message transmission.
*
* Enforces RFC 9218 §7.1: clients MUST treat inbound PRIORITY_UPDATE as a connection error.
*
* @since 5.0
*/
@Internal
Expand Down Expand Up @@ -94,6 +97,7 @@ void acceptHeaderFrame() throws H2ConnectionException {

@Override
void acceptPushFrame() throws H2ConnectionException {
// Allowed; server may send push streams if enabled.
}

@Override
Expand Down Expand Up @@ -135,6 +139,17 @@ H2StreamHandler createRemotelyInitiatedStream(
context);
}

/**
* RFC 9218 §7.1: clients MUST treat inbound PRIORITY_UPDATE as a connection error.
* @since 5.4
*/
@Override
protected void onPriorityUpdateFrame(final RawFrame frame) throws H2ConnectionException {
throw new H2ConnectionException(
H2Error.PROTOCOL_ERROR,
"Inbound PRIORITY_UPDATE is not permitted on client connections");
}

@Override
public String toString() {
final StringBuilder buf = new StringBuilder();
Expand All @@ -145,4 +160,3 @@ public String toString() {
}

}

Loading
Loading