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 @@ -43,7 +43,8 @@ public enum FrameType {
GOAWAY(0x07),
WINDOW_UPDATE(0x08),
CONTINUATION(0x09),
PRIORITY_UPDATE(0x10); // 16
PRIORITY_UPDATE(0x10), // 16
ORIGIN(0x0c); // RFC 8336

final int value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@
package org.apache.hc.core5.http2.impl.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
Expand All @@ -53,11 +58,13 @@
import org.apache.hc.core5.http.HttpConnection;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStreamResetException;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.ProtocolVersion;
import org.apache.hc.core5.http.RequestNotExecutedException;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.config.CharCodingConfig;
import org.apache.hc.core5.http.impl.BasicEndpointDetails;
import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
Expand Down Expand Up @@ -144,6 +151,12 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
private final Map<Integer, PriorityValue> priorities = new ConcurrentHashMap<>();
private volatile boolean peerNoRfc7540Priorities;

/**
* RFC 8336 Origin Set (client-side).
*/
private final Set<HttpHost> originSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
private volatile boolean originInit;

AbstractH2StreamMultiplexer(
final ProtocolIOSession ioSession,
final FrameFactory frameFactory,
Expand Down Expand Up @@ -729,6 +742,19 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "CONTINUATION frame expected");
}
switch (frameType) {
case ORIGIN: { // RFC 8336
// Only valid on stream 0; ignore on h2c; ignore reserved incompatible flags (0x1|0x2|0x4|0x8)
if (streamId == 0 && getSSLSession() != null) {
final int flags = frame.getFlags();
if ((flags & 0x0F) == 0) {
final ByteBuffer pl = frame.getPayloadContent();
if (pl != null) {
processOriginPayload(pl);
}
}
}
}
break;
case DATA: {
if (streamId == 0) {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id: " + streamId);
Expand Down Expand Up @@ -1584,4 +1610,117 @@ public String toString() {

}

/**
* Initialize the Origin Set once per connection (RFC 8336 §2.3).
*/
private void ensureOriginInit() {
if (originInit) {
return;
}
// Initial origin: scheme "https", host = SNI (lowercased) if available, else remote IP; port = remote port.
final SSLSession ssl = getSSLSession();
if (ssl == null) {
return; // ORIGIN is ignored on h2c; keep uninitialized
}
String host = null;
try {
// Best-effort SNI via session value used by our TLS strategy (if present)
final Object sni = ssl.getValue("HOSTNAME");
if (sni instanceof String) {
host = ((String) sni).toLowerCase(Locale.ROOT);
}
} catch (final Exception ignore) {
}
if (host == null) {
final SocketAddress ra = getRemoteAddress();
if (ra instanceof InetSocketAddress) {
host = ((InetSocketAddress) ra).getHostString().toLowerCase(Locale.ROOT);
}
}
int port = 0;
final SocketAddress ra = getRemoteAddress();
if (ra instanceof java.net.InetSocketAddress) {
port = ((java.net.InetSocketAddress) ra).getPort();
}
if (host != null && port > 0) {
originSet.add(new HttpHost("https", host, port));
originInit = true;
}
}

/**
* Parse and merge ORIGIN payload (list of Origin-Entry).
*/
private void processOriginPayload(final ByteBuffer pl) {
ensureOriginInit();
while (pl.remaining() >= 2) {
final int len = Short.toUnsignedInt(pl.getShort());
if (len == 0) {
// Empty Origin-Entry is allowed (server can signal "SNI-only"); no-op here.
continue;
}
if (pl.remaining() < len) {
break; // malformed; stop processing silently per robustness principle
}
final byte[] b = new byte[len];
pl.get(b);
final String ascii = new String(b, java.nio.charset.StandardCharsets.US_ASCII);
final org.apache.hc.core5.http.HttpHost parsed = parseAsciiOrigin(ascii);
if (parsed != null) {
originSet.add(parsed);
}
}
}

/**
* RFC 6454 ASCII origin parser (scheme/host/port only). Returns null if invalid.
*/
private HttpHost parseAsciiOrigin(final String s) {
try {
final java.net.URI u = java.net.URI.create(s);
if (u.getFragment() != null) return null;
final String scheme = u.getScheme();
final String host = u.getHost();
if (scheme == null || host == null) return null;
int port = u.getPort();
if (port < 0) {
if (URIScheme.HTTPS.same(scheme)) {
port = 443;
} else if (URIScheme.HTTP.same(scheme)) {
port = 80;
} else {
return null;
}
}
return new HttpHost(scheme.toLowerCase(Locale.ROOT), host.toLowerCase(Locale.ROOT), port);
} catch (final IllegalArgumentException ex) {
return null;
}
}


protected final void commitConnFrame(final RawFrame frame) throws IOException {
Args.notNull(frame, "Frame");
ioSession.getLock().lock();
try {
commitFrameInternal(frame);
} finally {
ioSession.getLock().unlock();
}
}

Set<HttpHost> getOriginSetSnapshot() {
return Collections.unmodifiableSet(new HashSet<>(originSet));
}

public void removeOrigin(final org.apache.hc.core5.http.HttpHost origin) {
if (origin != null) {
originSet.remove(origin);
}
}

boolean isOriginAllowed(final HttpHost origin) {
return origin != null && originSet.contains(origin);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

Expand All @@ -37,11 +38,13 @@
import org.apache.hc.core5.http.HeaderElements;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
import org.apache.hc.core5.http.impl.IncomingEntityDetails;
import org.apache.hc.core5.http.impl.nio.MessageState;
Expand All @@ -54,6 +57,7 @@
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http2.H2ConnectionException;
import org.apache.hc.core5.http2.H2Error;
import org.apache.hc.core5.http2.H2PseudoRequestHeaders;
import org.apache.hc.core5.http2.impl.DefaultH2RequestConverter;
import org.apache.hc.core5.http2.impl.DefaultH2ResponseConverter;

Expand All @@ -72,13 +76,18 @@ class ClientH2StreamHandler implements H2StreamHandler {
private final AtomicBoolean failed;
private final AtomicBoolean done;

private final ClientH2StreamMultiplexer parent;
private volatile HttpHost lastRequestOrigin;

ClientH2StreamHandler(
final ClientH2StreamMultiplexer parent,
final H2StreamChannel outputChannel,
final HttpProcessor httpProcessor,
final BasicHttpConnectionMetrics connMetrics,
final AsyncClientExchangeHandler exchangeHandler,
final HandlerFactory<AsyncPushConsumer> pushHandlerFactory,
final HttpCoreContext context) {
this.parent = parent;
this.outputChannel = outputChannel;
this.dataChannel = new DataStreamChannel() {

Expand Down Expand Up @@ -142,6 +151,35 @@ private void commitRequest(final HttpRequest request, final EntityDetails entity
httpProcessor.process(request, entityDetails, context);

final List<Header> headers = DefaultH2RequestConverter.INSTANCE.convert(request);
String scheme = null;
String authority = null;
for (final Header h : headers) {
final String n = h.getName();
if (H2PseudoRequestHeaders.SCHEME.equalsIgnoreCase(n)) {
scheme = h.getValue();
} else if (H2PseudoRequestHeaders.AUTHORITY.equalsIgnoreCase(n)) {
authority = h.getValue();
}
}
if (scheme != null && authority != null) {
String host = authority;
int port = -1;
final int colon = authority.lastIndexOf(':');
if (colon > 0 && authority.indexOf(']') < 0) {
host = authority.substring(0, colon);
try {
port = Integer.parseInt(authority.substring(colon + 1));
} catch (final NumberFormatException ignore) {

}
}
if (port < 0) {
port = URIScheme.HTTPS.same(scheme) ? 443 : (URIScheme.HTTP.same(scheme) ? 80 : -1);
}
if (port > 0) {
lastRequestOrigin = new HttpHost(scheme.toLowerCase(Locale.ROOT), host.toLowerCase(Locale.ROOT),port);
}
}
if (entityDetails == null) {
requestState.set(MessageState.COMPLETE);
outputChannel.submit(headers, true);
Expand Down Expand Up @@ -197,6 +235,16 @@ public void consumeHeader(final List<Header> headers, final boolean endStream) t
if (status > HttpStatus.SC_CONTINUE && status < HttpStatus.SC_SUCCESS) {
exchangeHandler.consumeInformation(response, context);
}
if (status == HttpStatus.SC_MISDIRECTED_REQUEST /* 421 */ && lastRequestOrigin != null && parent != null) {
parent.removeOrigin(lastRequestOrigin);
}
if (lastRequestOrigin != null) {
// Only enforce after ORIGIN has initialized the set (i.e., it's not empty)
if (!parent.getOriginSetSnapshot().isEmpty() && !parent.isOriginAllowed(lastRequestOrigin)) {
throw new ProtocolException("Origin not allowed on this HTTP/2 connection: " + lastRequestOrigin);
}
}

if (requestState.get() == MessageState.ACK) {
if (status == HttpStatus.SC_CONTINUE || status >= HttpStatus.SC_SUCCESS) {
requestState.set(MessageState.BODY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ H2StreamHandler outgoingRequest(
final HttpCoreContext coreContext = HttpCoreContext.castOrCreate(context);
coreContext.setSSLSession(getSSLSession());
coreContext.setEndpointDetails(getEndpointDetails());
return new ClientH2StreamHandler(channel, getHttpProcessor(), getConnMetrics(), exchangeHandler,
return new ClientH2StreamHandler(this, channel, getHttpProcessor(), getConnMetrics(), exchangeHandler,
pushHandlerFactory != null ? pushHandlerFactory : this.pushHandlerFactory,
coreContext);
}
Expand Down Expand Up @@ -170,6 +170,5 @@ public String toString() {
buf.append("]");
return buf.toString();
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.hc.core5.annotation.Internal;
Expand All @@ -50,6 +53,8 @@
import org.apache.hc.core5.http2.config.H2Setting;
import org.apache.hc.core5.http2.frame.DefaultFrameFactory;
import org.apache.hc.core5.http2.frame.FrameFactory;
import org.apache.hc.core5.http2.frame.FrameType;
import org.apache.hc.core5.http2.frame.RawFrame;
import org.apache.hc.core5.http2.frame.StreamIdGenerator;
import org.apache.hc.core5.http2.hpack.HeaderListConstraintException;
import org.apache.hc.core5.reactor.ProtocolIOSession;
Expand Down Expand Up @@ -171,4 +176,38 @@ public String toString() {
return buf.toString();
}


public void sendOrigin(final Collection<String> asciiOrigins) throws IOException {
if (asciiOrigins == null || asciiOrigins.isEmpty()) {
final ByteBuffer empty = ByteBuffer.allocate(0);
final RawFrame origin = new RawFrame(FrameType.ORIGIN.getValue(), 0, 0, empty);
commitConnFrame(origin);
return;
}
final ArrayList<byte[]> parts = new ArrayList<>();
int total = 0;
for (final String s : asciiOrigins) {
if (s == null) {
continue;
}
final byte[] b = s.getBytes(StandardCharsets.US_ASCII);
if (b.length > 0xFFFF) {
continue;
}
parts.add(b);
total += 2 + b.length;
}
if (total == 0) {
return;
}
final ByteBuffer pl = ByteBuffer.allocate(total);
for (final byte[] b : parts) {
pl.putShort((short)(b.length & 0xFFFF));
pl.put(b);
}
pl.flip();
final RawFrame origin = new RawFrame(FrameType.ORIGIN.getValue(), 0, 0, pl);
commitConnFrame(origin);
}

}
Loading