Merged branch 'jetty-11.0.x' into 'jetty-12.0.x'.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2023-05-28 00:34:49 +02:00
parent 476145b817
commit 8c4e75bf8d
No known key found for this signature in database
GPG Key ID: 1677D141BCF3584D
98 changed files with 2795 additions and 736 deletions

View File

@ -141,6 +141,7 @@ public class HttpClient extends ContainerLifeCycle
private String defaultRequestContentType = "application/octet-stream";
private boolean useInputDirectByteBuffers = true;
private boolean useOutputDirectByteBuffers = true;
private int maxResponseHeadersSize = -1;
private Sweeper destinationSweeper;
/**
@ -1111,6 +1112,23 @@ public class HttpClient extends ContainerLifeCycle
this.useOutputDirectByteBuffers = useOutputDirectByteBuffers;
}
/**
* @return the max size in bytes of the response headers
*/
@ManagedAttribute("The max size in bytes of the response headers")
public int getMaxResponseHeadersSize()
{
return maxResponseHeadersSize;
}
/**
* @param maxResponseHeadersSize the max size in bytes of the response headers
*/
public void setMaxResponseHeadersSize(int maxResponseHeadersSize)
{
this.maxResponseHeadersSize = maxResponseHeadersSize;
}
/**
* @return the forward proxy configuration
*/

View File

@ -59,7 +59,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
{
super(channel);
HttpClient httpClient = channel.getHttpDestination().getHttpClient();
parser = new HttpParser(this, -1, httpClient.getHttpCompliance());
parser = new HttpParser(this, httpClient.getMaxResponseHeadersSize(), httpClient.getHttpCompliance());
HttpClientTransport transport = httpClient.getTransport();
if (transport instanceof HttpClientTransportOverHTTP httpTransport)
{

View File

@ -3,7 +3,7 @@ Since OpenJDK 13.0.2/11.0.6 it is required that CA certificates have the extensi
The keystores are generated in the following way:
# Generates the server keystore. Note the BasicConstraint=CA:true extension.
$ keytool -v -genkeypair -validity 36500 -keyalg RSA -keysize 2048 -keystore keystore.p12 -storetype pkcs12 -dname "CN=localhost, OU=Jetty, O=Webtide, L=Omaha, S=NE, C=US" -ext bc=ca:true -ext san=ip:127.0.0.1,ip:[::1]
$ keytool -v -genkeypair -validity 36500 -keyalg RSA -keysize 2048 -keystore keystore.p12 -storetype pkcs12 -dname "CN=localhost, OU=Jetty, O=Webtide, L=Omaha, S=NE, C=US" -ext bc=ca:true -ext san=ip:127.0.0.1,ip:[::1],dns:localhost
# Export the server certificate.
$ keytool -v -export -keystore keystore.p12 -rfc -file server.crt

View File

@ -178,10 +178,10 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
HttpRequest pushRequest = (HttpRequest)getHttpDestination().getHttpClient().newRequest(metaData.getHttpURI().toString());
// TODO: copy PUSH_PROMISE headers into pushRequest.
BiFunction<Request, Request, Response.CompleteListener> pushListener = request.getPushHandler();
if (pushListener != null)
BiFunction<Request, Request, Response.CompleteListener> pushHandler = request.getPushHandler();
if (pushHandler != null)
{
Response.CompleteListener listener = pushListener.apply(request, pushRequest);
Response.CompleteListener listener = pushHandler.apply(request, pushRequest);
if (listener != null)
{
HttpChannelOverHTTP2 pushChannel = getHttpChannel().getHttpConnection().acquireHttpChannel();

View File

@ -28,6 +28,7 @@ import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.hpack.HpackContext;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
@ -106,11 +107,13 @@ public class HTTP2Client extends ContainerLifeCycle
private List<String> protocols = List.of("h2");
private int initialSessionRecvWindow = 16 * 1024 * 1024;
private int initialStreamRecvWindow = 8 * 1024 * 1024;
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
private int maxFrameSize = Frame.DEFAULT_MAX_SIZE;
private int maxConcurrentPushedStreams = 32;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private int maxDynamicTableSize = 4096;
private int maxDecoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxEncoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxHeaderBlockFragment = 0;
private int maxResponseHeadersSize = -1;
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
private long streamIdleTimeout;
private boolean useInputDirectByteBuffers = true;
@ -282,15 +285,15 @@ public class HTTP2Client extends ContainerLifeCycle
this.initialStreamRecvWindow = initialStreamRecvWindow;
}
@ManagedAttribute("The max frame length in bytes")
public int getMaxFrameLength()
@ManagedAttribute("The max frame size in bytes")
public int getMaxFrameSize()
{
return maxFrameLength;
return maxFrameSize;
}
public void setMaxFrameLength(int maxFrameLength)
public void setMaxFrameSize(int maxFrameSize)
{
this.maxFrameLength = maxFrameLength;
this.maxFrameSize = maxFrameSize;
}
@ManagedAttribute("The max number of concurrent pushed streams")
@ -315,15 +318,32 @@ public class HTTP2Client extends ContainerLifeCycle
this.maxSettingsKeys = maxSettingsKeys;
}
@ManagedAttribute("The HPACK dynamic table maximum size")
public int getMaxDynamicTableSize()
@ManagedAttribute("The HPACK encoder dynamic table maximum capacity")
public int getMaxEncoderTableCapacity()
{
return maxDynamicTableSize;
return maxEncoderTableCapacity;
}
public void setMaxDynamicTableSize(int maxDynamicTableSize)
/**
* <p>Sets the limit for the encoder HPACK dynamic table capacity.</p>
* <p>Setting this value to {@code 0} disables the use of the dynamic table.</p>
*
* @param maxEncoderTableCapacity The HPACK encoder dynamic table maximum capacity
*/
public void setMaxEncoderTableCapacity(int maxEncoderTableCapacity)
{
this.maxDynamicTableSize = maxDynamicTableSize;
this.maxEncoderTableCapacity = maxEncoderTableCapacity;
}
@ManagedAttribute("The HPACK decoder dynamic table maximum capacity")
public int getMaxDecoderTableCapacity()
{
return maxDecoderTableCapacity;
}
public void setMaxDecoderTableCapacity(int maxDecoderTableCapacity)
{
this.maxDecoderTableCapacity = maxDecoderTableCapacity;
}
@ManagedAttribute("The max size of header block fragments")
@ -337,6 +357,17 @@ public class HTTP2Client extends ContainerLifeCycle
this.maxHeaderBlockFragment = maxHeaderBlockFragment;
}
@ManagedAttribute("The max size of response headers")
public int getMaxResponseHeadersSize()
{
return maxResponseHeadersSize;
}
public void setMaxResponseHeadersSize(int maxResponseHeadersSize)
{
this.maxResponseHeadersSize = maxResponseHeadersSize;
}
@ManagedAttribute("Whether to use direct ByteBuffers for reading")
public boolean isUseInputDirectByteBuffers()
{

View File

@ -16,17 +16,18 @@ package org.eclipse.jetty.http2.client;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.HTTP2Connection;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.client.internal.HTTP2ClientSession;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.PrefaceFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.hpack.HpackContext;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory;
@ -34,7 +35,6 @@ import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.thread.Scheduler;
public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
{
@ -49,29 +49,28 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
{
HTTP2Client client = (HTTP2Client)context.get(CLIENT_CONTEXT_KEY);
ByteBufferPool bufferPool = client.getByteBufferPool();
Executor executor = client.getExecutor();
Scheduler scheduler = client.getScheduler();
Session.Listener listener = (Session.Listener)context.get(SESSION_LISTENER_CONTEXT_KEY);
@SuppressWarnings("unchecked")
Promise<Session> promise = (Promise<Session>)context.get(SESSION_PROMISE_CONTEXT_KEY);
Promise<Session> sessionPromise = (Promise<Session>)context.get(SESSION_PROMISE_CONTEXT_KEY);
Generator generator = new Generator(bufferPool, client.getMaxDynamicTableSize(), client.getMaxHeaderBlockFragment());
Generator generator = new Generator(bufferPool, client.isUseOutputDirectByteBuffers(), client.getMaxHeaderBlockFragment());
FlowControlStrategy flowControl = client.getFlowControlStrategyFactory().newFlowControlStrategy();
HTTP2ClientSession session = new HTTP2ClientSession(scheduler, endPoint, generator, listener, flowControl);
Parser parser = new Parser(bufferPool, client.getMaxResponseHeadersSize());
parser.setMaxFrameSize(client.getMaxFrameSize());
parser.setMaxSettingsKeys(client.getMaxSettingsKeys());
HTTP2ClientSession session = new HTTP2ClientSession(client.getScheduler(), endPoint, parser, generator, listener, flowControl);
session.setMaxRemoteStreams(client.getMaxConcurrentPushedStreams());
session.setMaxEncoderTableCapacity(client.getMaxEncoderTableCapacity());
long streamIdleTimeout = client.getStreamIdleTimeout();
if (streamIdleTimeout > 0)
session.setStreamIdleTimeout(streamIdleTimeout);
Parser parser = new Parser(bufferPool, session, 4096, 8192);
parser.setMaxFrameLength(client.getMaxFrameLength());
parser.setMaxSettingsKeys(client.getMaxSettingsKeys());
HTTP2ClientConnection connection = new HTTP2ClientConnection(client, bufferPool, executor, endPoint,
parser, session, client.getInputBufferSize(), promise, listener);
connection.setUseInputDirectByteBuffers(client.isUseInputDirectByteBuffers());
connection.setUseOutputDirectByteBuffers(client.isUseOutputDirectByteBuffers());
HTTP2ClientConnection connection = new HTTP2ClientConnection(client, endPoint, session, sessionPromise, listener);
connection.addEventListener(connectionListener);
parser.init(connection);
return customize(connection, context);
}
@ -81,12 +80,14 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
private final Promise<Session> promise;
private final Session.Listener listener;
private HTTP2ClientConnection(HTTP2Client client, ByteBufferPool byteBufferPool, Executor executor, EndPoint endpoint, Parser parser, HTTP2Session session, int bufferSize, Promise<Session> promise, Session.Listener listener)
private HTTP2ClientConnection(HTTP2Client client, EndPoint endpoint, HTTP2ClientSession session, Promise<Session> sessionPromise, Session.Listener listener)
{
super(byteBufferPool, executor, endpoint, parser, session, bufferSize);
super(client.getByteBufferPool(), client.getExecutor(), endpoint, session, client.getInputBufferSize());
this.client = client;
this.promise = promise;
this.promise = sessionPromise;
this.listener = listener;
setUseInputDirectByteBuffers(client.isUseInputDirectByteBuffers());
setUseOutputDirectByteBuffers(client.isUseOutputDirectByteBuffers());
}
@Override
@ -95,12 +96,52 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
Map<Integer, Integer> settings = listener.onPreface(getSession());
if (settings == null)
settings = new HashMap<>();
settings.computeIfAbsent(SettingsFrame.INITIAL_WINDOW_SIZE, k -> client.getInitialStreamRecvWindow());
settings.computeIfAbsent(SettingsFrame.MAX_CONCURRENT_STREAMS, k -> client.getMaxConcurrentPushedStreams());
Integer maxFrameLength = settings.get(SettingsFrame.MAX_FRAME_SIZE);
if (maxFrameLength != null)
getParser().setMaxFrameLength(maxFrameLength);
// Below we want to populate any settings to send to the server
// that have a different default than what prescribed by the RFC.
// Changing the configuration is done when the SETTINGS is sent.
settings.compute(SettingsFrame.HEADER_TABLE_SIZE, (k, v) ->
{
if (v == null)
{
v = client.getMaxDecoderTableCapacity();
if (v == HpackContext.DEFAULT_MAX_TABLE_CAPACITY)
v = null;
}
return v;
});
settings.computeIfAbsent(SettingsFrame.MAX_CONCURRENT_STREAMS, k -> client.getMaxConcurrentPushedStreams());
settings.compute(SettingsFrame.INITIAL_WINDOW_SIZE, (k, v) ->
{
if (v == null)
{
v = client.getInitialStreamRecvWindow();
if (v == FlowControlStrategy.DEFAULT_WINDOW_SIZE)
v = null;
}
return v;
});
settings.compute(SettingsFrame.MAX_FRAME_SIZE, (k, v) ->
{
if (v == null)
{
v = client.getMaxFrameSize();
if (v == Frame.DEFAULT_MAX_SIZE)
v = null;
}
return v;
});
settings.compute(SettingsFrame.MAX_HEADER_LIST_SIZE, (k, v) ->
{
if (v == null)
{
v = client.getMaxResponseHeadersSize();
if (v <= 0)
v = null;
}
return v;
});
PrefaceFrame prefaceFrame = new PrefaceFrame();
SettingsFrame settingsFrame = new SettingsFrame(settings, false);

View File

@ -13,6 +13,8 @@
package org.eclipse.jetty.http2.client.internal;
import java.util.Map;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.FlowControlStrategy;
@ -22,7 +24,9 @@ import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.Scheduler;
@ -33,9 +37,9 @@ public class HTTP2ClientSession extends HTTP2Session
{
private static final Logger LOG = LoggerFactory.getLogger(HTTP2ClientSession.class);
public HTTP2ClientSession(Scheduler scheduler, EndPoint endPoint, Generator generator, Session.Listener listener, FlowControlStrategy flowControl)
public HTTP2ClientSession(Scheduler scheduler, EndPoint endPoint, Parser parser, Generator generator, Session.Listener listener, FlowControlStrategy flowControl)
{
super(scheduler, endPoint, generator, listener, flowControl, 1);
super(scheduler, endPoint, parser, generator, listener, flowControl, 1);
}
@Override
@ -78,12 +82,30 @@ public class HTTP2ClientSession extends HTTP2Session
}
}
@Override
public void onSettings(SettingsFrame frame)
{
Map<Integer, Integer> settings = frame.getSettings();
Integer value = settings.get(SettingsFrame.ENABLE_PUSH);
// SPEC: servers can only send ENABLE_PUSH=0.
if (value != null && value != 0)
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_frame");
else
super.onSettings(frame);
}
@Override
public void onPushPromise(PushPromiseFrame frame)
{
if (LOG.isDebugEnabled())
LOG.debug("Received {}", frame);
if (!isPushEnabled())
{
onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_push_promise_frame");
return;
}
int streamId = frame.getStreamId();
int pushStreamId = frame.getPromisedStreamId();
HTTP2Stream stream = getStream(streamId);

View File

@ -22,6 +22,14 @@ import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
@ -39,7 +47,7 @@ import org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HTTP2Connection extends AbstractConnection implements WriteFlusher.Listener, Connection.UpgradeTo
public class HTTP2Connection extends AbstractConnection implements Parser.Listener, WriteFlusher.Listener, Connection.UpgradeTo
{
private static final Logger LOG = LoggerFactory.getLogger(HTTP2Connection.class);
@ -47,24 +55,21 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
private final Queue<Runnable> tasks = new ArrayDeque<>();
private final HTTP2Producer producer = new HTTP2Producer();
private final AtomicLong bytesIn = new AtomicLong();
private final ByteBufferPool byteBufferPool;
private final Parser parser;
private final ByteBufferPool bufferPool;
private final HTTP2Session session;
private final int bufferSize;
private final ExecutionStrategy strategy;
private boolean useInputDirectByteBuffers;
private boolean useOutputDirectByteBuffers;
protected HTTP2Connection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, Parser parser, HTTP2Session session, int bufferSize)
protected HTTP2Connection(ByteBufferPool bufferPool, Executor executor, EndPoint endPoint, HTTP2Session session, int bufferSize)
{
super(endPoint, executor);
this.byteBufferPool = byteBufferPool;
this.parser = parser;
this.bufferPool = bufferPool;
this.session = session;
this.bufferSize = bufferSize;
this.strategy = new AdaptiveExecutionStrategy(producer, executor);
LifeCycle.start(strategy);
parser.init(ParserListener::new);
}
@Override
@ -98,11 +103,6 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
return session;
}
protected Parser getParser()
{
return parser;
}
@Override
public void onUpgradeTo(ByteBuffer buffer)
{
@ -233,6 +233,74 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
}
}
@Override
public void onHeaders(HeadersFrame frame)
{
session.onHeaders(frame);
}
@Override
public void onData(DataFrame frame)
{
NetworkBuffer networkBuffer = producer.networkBuffer;
session.onData(new StreamData(frame, networkBuffer));
}
@Override
public void onPriority(PriorityFrame frame)
{
session.onPriority(frame);
}
@Override
public void onReset(ResetFrame frame)
{
session.onReset(frame);
}
@Override
public void onSettings(SettingsFrame frame)
{
session.onSettings(frame);
}
@Override
public void onPushPromise(PushPromiseFrame frame)
{
session.onPushPromise(frame);
}
@Override
public void onPing(PingFrame frame)
{
session.onPing(frame);
}
@Override
public void onGoAway(GoAwayFrame frame)
{
session.onGoAway(frame);
}
@Override
public void onWindowUpdate(WindowUpdateFrame frame)
{
session.onWindowUpdate(frame);
}
@Override
public void onStreamFailure(int streamId, int error, String reason)
{
session.onStreamFailure(streamId, error, reason);
}
@Override
public void onConnectionFailure(int error, String reason)
{
producer.failed = true;
session.onConnectionFailure(error, reason);
}
@Override
public void onFlushed(long bytes) throws IOException
{
@ -277,7 +345,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
{
while (networkBuffer.hasRemaining())
{
parser.parse(networkBuffer.getBuffer());
session.getParser().parse(networkBuffer.getBuffer());
if (failed)
return null;
}
@ -393,28 +461,6 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
}
}
private class ParserListener extends Parser.Listener.Wrapper
{
private ParserListener(Parser.Listener listener)
{
super(listener);
}
@Override
public void onData(DataFrame frame)
{
NetworkBuffer networkBuffer = producer.networkBuffer;
session.onData(new StreamData(frame, networkBuffer));
}
@Override
public void onConnectionFailure(int error, String reason)
{
producer.failed = true;
super.onConnectionFailure(error, reason);
}
}
private static class StreamData extends Stream.Data
{
private final Retainable retainable;
@ -450,7 +496,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
private NetworkBuffer()
{
delegate = byteBufferPool.acquire(bufferSize, isUseInputDirectByteBuffers());
delegate = bufferPool.acquire(bufferSize, isUseInputDirectByteBuffers());
}
public ByteBuffer getBuffer()

View File

@ -53,6 +53,7 @@ import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.StreamFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.internal.HTTP2Flusher;
import org.eclipse.jetty.http2.parser.Parser;
@ -94,6 +95,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
private final AtomicInteger recvWindow = new AtomicInteger();
private final AtomicLong bytesWritten = new AtomicLong();
private final EndPoint endPoint;
private final Parser parser;
private final Generator generator;
private final Session.Listener listener;
private final FlowControlStrategy flowControl;
@ -104,12 +106,14 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
private long streamIdleTimeout;
private int initialSessionRecvWindow;
private int writeThreshold;
private int maxEncoderTableCapacity;
private boolean pushEnabled;
private boolean connectProtocolEnabled;
public HTTP2Session(Scheduler scheduler, EndPoint endPoint, Generator generator, Session.Listener listener, FlowControlStrategy flowControl, int initialStreamId)
public HTTP2Session(Scheduler scheduler, EndPoint endPoint, Parser parser, Generator generator, Session.Listener listener, FlowControlStrategy flowControl, int initialStreamId)
{
this.endPoint = endPoint;
this.parser = parser;
this.generator = generator;
this.listener = listener;
this.flowControl = flowControl;
@ -207,11 +211,27 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
this.writeThreshold = writeThreshold;
}
@ManagedAttribute("The HPACK encoder dynamic table maximum capacity")
public int getMaxEncoderTableCapacity()
{
return maxEncoderTableCapacity;
}
public void setMaxEncoderTableCapacity(int maxEncoderTableCapacity)
{
this.maxEncoderTableCapacity = maxEncoderTableCapacity;
}
public EndPoint getEndPoint()
{
return endPoint;
}
public Parser getParser()
{
return parser;
}
public Generator getGenerator()
{
return generator;
@ -352,8 +372,20 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
if (frame.isReply())
return;
// Iterate over all settings
for (Map.Entry<Integer, Integer> entry : frame.getSettings().entrySet())
Map<Integer, Integer> settings = frame.getSettings();
configure(settings, false);
notifySettings(this, frame);
if (reply)
{
SettingsFrame replyFrame = new SettingsFrame(Collections.emptyMap(), true);
settings(replyFrame, Callback.NOOP);
}
}
private void configure(Map<Integer, Integer> settings, boolean local)
{
for (Map.Entry<Integer, Integer> entry : settings.entrySet())
{
int key = entry.getKey();
int value = entry.getValue();
@ -362,8 +394,17 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
case SettingsFrame.HEADER_TABLE_SIZE ->
{
if (LOG.isDebugEnabled())
LOG.debug("Updating HPACK header table size to {} for {}", value, this);
generator.setHeaderTableSize(value);
LOG.debug("Updating HPACK {} max table capacity to {} for {}", local ? "decoder" : "encoder", value, this);
if (local)
{
parser.getHpackDecoder().setMaxTableCapacity(value);
}
else
{
HpackEncoder hpackEncoder = generator.getHpackEncoder();
hpackEncoder.setMaxTableCapacity(value);
hpackEncoder.setTableCapacity(Math.min(value, getMaxEncoderTableCapacity()));
}
}
case SettingsFrame.ENABLE_PUSH ->
{
@ -375,26 +416,35 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
case SettingsFrame.MAX_CONCURRENT_STREAMS ->
{
if (LOG.isDebugEnabled())
LOG.debug("Updating max local concurrent streams to {} for {}", value, this);
maxLocalStreams = value;
LOG.debug("Updating max {} concurrent streams to {} for {}", local ? "remote" : "local", value, this);
if (local)
maxRemoteStreams = value;
else
maxLocalStreams = value;
}
case SettingsFrame.INITIAL_WINDOW_SIZE ->
{
if (LOG.isDebugEnabled())
LOG.debug("Updating initial stream window size to {} for {}", value, this);
flowControl.updateInitialStreamWindow(this, value, false);
flowControl.updateInitialStreamWindow(this, value, local);
}
case SettingsFrame.MAX_FRAME_SIZE ->
{
if (LOG.isDebugEnabled())
LOG.debug("Updating max frame size to {} for {}", value, this);
generator.setMaxFrameSize(value);
LOG.debug("Updating {} max frame size to {} for {}", local ? "parser" : "generator", value, this);
if (local)
parser.setMaxFrameSize(value);
else
generator.setMaxFrameSize(value);
}
case SettingsFrame.MAX_HEADER_LIST_SIZE ->
{
if (LOG.isDebugEnabled())
LOG.debug("Updating max header list size to {} for {}", value, this);
generator.setMaxHeaderListSize(value);
LOG.debug("Updating {} max header list size to {} for {}", local ? "decoder" : "encoder", value, this);
if (local)
parser.getHpackDecoder().setMaxHeaderListSize(value);
else
generator.getHpackEncoder().setMaxHeaderListSize(value);
}
case SettingsFrame.ENABLE_CONNECT_PROTOCOL ->
{
@ -410,13 +460,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
}
}
}
notifySettings(this, frame);
if (reply)
{
SettingsFrame replyFrame = new SettingsFrame(Collections.emptyMap(), true);
settings(replyFrame, Callback.NOOP);
}
}
@Override
@ -622,6 +665,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
public void push(Stream stream, Promise<Stream> promise, PushPromiseFrame frame, Stream.Listener listener)
{
if (!isPushEnabled())
throw new IllegalStateException("Push is disabled");
streamsState.push(frame, new Promise.Wrapper<>(promise)
{
@Override
@ -1343,9 +1388,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements Session
case SETTINGS ->
{
SettingsFrame settingsFrame = (SettingsFrame)frame;
Integer initialWindow = settingsFrame.getSettings().get(SettingsFrame.INITIAL_WINDOW_SIZE);
if (initialWindow != null)
flowControl.updateInitialStreamWindow(HTTP2Session.this, initialWindow, true);
if (!settingsFrame.isReply())
configure(settingsFrame.getSettings(), true);
}
}
}

View File

@ -438,8 +438,16 @@ public class HTTP2Stream implements Stream, Attachable, Closeable, Callback, Dum
}
}
if (offer(data))
processData();
if (getListener() != null)
{
if (offer(data))
processData();
}
else
{
if (updateClose(data.frame().isEndStream(), CloseState.Event.RECEIVED))
session.removeStream(this);
}
}
private boolean offer(Data data)
@ -904,9 +912,8 @@ public class HTTP2Stream implements Stream, Attachable, Closeable, Callback, Dum
@Override
public String toString()
{
return String.format("%s@%x#%d@%x{sendWindow=%s,recvWindow=%s,queue=%d,demand=%b,reset=%b/%b,%s,age=%d,attachment=%s}",
return String.format("%s#%d@%x{sendWindow=%s,recvWindow=%s,queue=%d,demand=%b,reset=%b/%b,%s,age=%d,attachment=%s}",
getClass().getSimpleName(),
hashCode(),
getId(),
session.hashCode(),
sendWindow,

View File

@ -16,8 +16,8 @@ package org.eclipse.jetty.http2.frames;
public abstract class Frame
{
public static final int HEADER_LENGTH = 9;
public static final int DEFAULT_MAX_LENGTH = 0x40_00;
public static final int MAX_MAX_LENGTH = 0xFF_FF_FF;
public static final int DEFAULT_MAX_SIZE = 0x40_00;
public static final int MAX_MAX_SIZE = 0xFF_FF_FF;
public static final Frame[] EMPTY_ARRAY = new Frame[0];
private final FrameType type;

View File

@ -30,20 +30,20 @@ public class Generator
public Generator(ByteBufferPool bufferPool)
{
this(bufferPool, 4096, 0);
this(bufferPool, 0);
}
public Generator(ByteBufferPool bufferPool, int maxDynamicTableSize, int maxHeaderBlockFragment)
public Generator(ByteBufferPool bufferPool, int maxHeaderBlockFragment)
{
this(bufferPool, true, maxDynamicTableSize, maxHeaderBlockFragment);
this(bufferPool, true, maxHeaderBlockFragment);
}
public Generator(ByteBufferPool bufferPool, boolean useDirectByteBuffers, int maxDynamicTableSize, int maxHeaderBlockFragment)
public Generator(ByteBufferPool bufferPool, boolean useDirectByteBuffers, int maxHeaderBlockFragment)
{
this.bufferPool = bufferPool;
headerGenerator = new HeaderGenerator(bufferPool, useDirectByteBuffers);
hpackEncoder = new HpackEncoder(maxDynamicTableSize);
hpackEncoder = new HpackEncoder();
this.generators = new FrameGenerator[FrameType.values().length];
this.generators[FrameType.HEADERS.getType()] = new HeadersGenerator(headerGenerator, hpackEncoder, maxHeaderBlockFragment);
@ -66,14 +66,9 @@ public class Generator
return bufferPool;
}
public void setValidateHpackEncoding(boolean validateEncoding)
public HpackEncoder getHpackEncoder()
{
hpackEncoder.setValidateEncoding(validateEncoding);
}
public void setHeaderTableSize(int headerTableSize)
{
hpackEncoder.setRemoteMaxDynamicTableSize(headerTableSize);
return hpackEncoder;
}
public void setMaxFrameSize(int maxFrameSize)
@ -90,9 +85,4 @@ public class Generator
{
return dataGenerator.generate(accumulator, frame, maxLength);
}
public void setMaxHeaderListSize(int value)
{
hpackEncoder.setMaxHeaderListSize(value);
}
}

View File

@ -47,7 +47,7 @@ public class GoAwayGenerator extends FrameGenerator
int fixedLength = 4 + 4;
// Make sure we don't exceed the default frame max length.
int maxPayloadLength = Frame.DEFAULT_MAX_LENGTH - fixedLength;
int maxPayloadLength = Frame.DEFAULT_MAX_SIZE - fixedLength;
if (payload != null && payload.length > maxPayloadLength)
payload = Arrays.copyOfRange(payload, 0, maxPayloadLength);

View File

@ -23,7 +23,7 @@ import org.eclipse.jetty.util.BufferUtil;
public class HeaderGenerator
{
private int maxFrameSize = Frame.DEFAULT_MAX_LENGTH;
private int maxFrameSize = Frame.DEFAULT_MAX_SIZE;
private final ByteBufferPool bufferPool;
private final boolean useDirectByteBuffers;

View File

@ -51,6 +51,9 @@ public class GoAwayBodyParser extends BodyParser
{
case PREPARE:
{
// SPEC: wrong streamId is treated as connection error.
if (getStreamId() != 0)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_go_away_frame");
state = State.LAST_STREAM_ID;
length = getBodyLength();
break;

View File

@ -76,7 +76,7 @@ public class HeaderParser
length = (length << 8) + octet;
if (++cursor == 3)
{
length &= Frame.MAX_MAX_LENGTH;
length &= Frame.MAX_MAX_SIZE;
state = State.TYPE;
}
break;

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.http2.parser;
import java.nio.ByteBuffer;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.RateControl;
@ -45,33 +44,34 @@ public class Parser
private static final Logger LOG = LoggerFactory.getLogger(Parser.class);
private final ByteBufferPool bufferPool;
private final Listener listener;
private final HeaderParser headerParser;
private final HpackDecoder hpackDecoder;
private final BodyParser[] bodyParsers;
private Listener listener;
private UnknownBodyParser unknownBodyParser;
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
private int maxFrameSize = Frame.DEFAULT_MAX_SIZE;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private boolean continuation;
private State state = State.HEADER;
public Parser(ByteBufferPool bufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize)
public Parser(ByteBufferPool bufferPool, int maxHeaderSize)
{
this(bufferPool, listener, maxDynamicTableSize, maxHeaderSize, RateControl.NO_RATE_CONTROL);
this(bufferPool, maxHeaderSize, RateControl.NO_RATE_CONTROL);
}
public Parser(ByteBufferPool bufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize, RateControl rateControl)
public Parser(ByteBufferPool bufferPool, int maxHeaderSize, RateControl rateControl)
{
this.bufferPool = bufferPool;
this.listener = listener;
this.headerParser = new HeaderParser(rateControl == null ? RateControl.NO_RATE_CONTROL : rateControl);
this.hpackDecoder = new HpackDecoder(maxDynamicTableSize, maxHeaderSize);
this.hpackDecoder = new HpackDecoder(maxHeaderSize);
this.bodyParsers = new BodyParser[FrameType.values().length];
}
public void init(UnaryOperator<Listener> wrapper)
public void init(Listener listener)
{
Listener listener = wrapper.apply(this.listener);
if (this.listener != null)
throw new IllegalStateException("Invalid parser initialization");
this.listener = listener;
unknownBodyParser = new UnknownBodyParser(headerParser, listener);
HeaderBlockParser headerBlockParser = new HeaderBlockParser(headerParser, bufferPool, hpackDecoder, unknownBodyParser);
HeaderBlockFragments headerBlockFragments = new HeaderBlockFragments(bufferPool);
@ -87,6 +87,16 @@ public class Parser
bodyParsers[FrameType.CONTINUATION.getType()] = new ContinuationBodyParser(headerParser, listener, headerBlockParser, headerBlockFragments);
}
protected Listener getListener()
{
return listener;
}
public HpackDecoder getHpackDecoder()
{
return hpackDecoder;
}
private void reset()
{
headerParser.reset();
@ -147,7 +157,7 @@ public class Parser
if (LOG.isDebugEnabled())
LOG.debug("Parsed {} frame header from {}@{}", headerParser, buffer, Integer.toHexString(buffer.hashCode()));
if (headerParser.getLength() > getMaxFrameLength())
if (headerParser.getLength() > getMaxFrameSize())
return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR, "invalid_frame_length");
FrameType frameType = FrameType.from(getFrameType());
@ -215,14 +225,14 @@ public class Parser
return headerParser.hasFlag(bit);
}
public int getMaxFrameLength()
public int getMaxFrameSize()
{
return maxFrameLength;
return maxFrameSize;
}
public void setMaxFrameLength(int maxFrameLength)
public void setMaxFrameSize(int maxFrameSize)
{
this.maxFrameLength = maxFrameLength;
this.maxFrameSize = maxFrameSize;
}
public int getMaxSettingsKeys()

View File

@ -28,18 +28,28 @@ public class ServerParser extends Parser
{
private static final Logger LOG = LoggerFactory.getLogger(ServerParser.class);
private final Listener listener;
private final PrefaceParser prefaceParser;
private PrefaceParser prefaceParser;
private State state = State.PREFACE;
private boolean notifyPreface = true;
public ServerParser(ByteBufferPool bufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize, RateControl rateControl)
public ServerParser(ByteBufferPool bufferPool, int maxHeaderSize, RateControl rateControl)
{
super(bufferPool, listener, maxDynamicTableSize, maxHeaderSize, rateControl);
this.listener = listener;
super(bufferPool, maxHeaderSize, rateControl);
}
@Override
public void init(Parser.Listener listener)
{
super.init(listener);
this.prefaceParser = new PrefaceParser(listener);
}
@Override
protected Listener getListener()
{
return (Listener)super.getListener();
}
/**
* <p>A direct upgrade is an unofficial upgrade from HTTP/1.1 to HTTP/2.0.</p>
* <p>A direct upgrade is initiated when {@code org.eclipse.jetty.server.HttpConnection}
@ -133,6 +143,7 @@ public class ServerParser extends Parser
private void notifyPreface()
{
Listener listener = getListener();
try
{
listener.onPreface();

View File

@ -69,6 +69,8 @@ public class SettingsBodyParser extends BodyParser
@Override
protected void emptyBody(ByteBuffer buffer)
{
if (!validateFrame(buffer, getStreamId(), 0))
return;
boolean isReply = hasFlag(Flags.ACK);
SettingsFrame frame = new SettingsFrame(Collections.emptyMap(), isReply);
if (!isReply && !rateControlOnEvent(frame))
@ -77,6 +79,17 @@ public class SettingsBodyParser extends BodyParser
onSettings(frame);
}
private boolean validateFrame(ByteBuffer buffer, int streamId, int bodyLength)
{
// SPEC: wrong streamId is treated as connection error.
if (streamId != 0)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_frame");
// SPEC: reply with body is treated as connection error.
if (hasFlag(Flags.ACK) && bodyLength > 0)
return connectionFailure(buffer, ErrorCode.FRAME_SIZE_ERROR.code, "invalid_settings_frame");
return true;
}
@Override
public boolean parse(ByteBuffer buffer)
{
@ -91,9 +104,8 @@ public class SettingsBodyParser extends BodyParser
{
case PREPARE:
{
// SPEC: wrong streamId is treated as connection error.
if (streamId != 0)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_frame");
if (!validateFrame(buffer, streamId, bodyLength))
return false;
length = bodyLength;
settings = new HashMap<>();
state = State.SETTING_ID;
@ -202,8 +214,8 @@ public class SettingsBodyParser extends BodyParser
if (initialWindowSize != null && initialWindowSize < 0)
return connectionFailure(buffer, ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_settings_initial_window_size");
Integer maxFrameLength = settings.get(SettingsFrame.MAX_FRAME_SIZE);
if (maxFrameLength != null && (maxFrameLength < Frame.DEFAULT_MAX_LENGTH || maxFrameLength > Frame.MAX_MAX_LENGTH))
Integer maxFrameSize = settings.get(SettingsFrame.MAX_FRAME_SIZE);
if (maxFrameSize != null && (maxFrameSize < Frame.DEFAULT_MAX_SIZE || maxFrameSize > Frame.MAX_MAX_SIZE))
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size");
SettingsFrame frame = new SettingsFrame(settings, hasFlag(Flags.ACK));

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpField;
@ -46,7 +45,8 @@ public class ContinuationParseTest
HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(bufferPool), new HpackEncoder());
final List<HeadersFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
@ -59,8 +59,7 @@ public class ContinuationParseTest
{
frames.add(new HeadersFrame(null, null, false));
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure the parser is properly reset.
for (int i = 0; i < 2; ++i)

View File

@ -17,7 +17,6 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.generator.DataGenerator;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
@ -88,15 +87,15 @@ public class DataGenerateParseTest
DataGenerator generator = new DataGenerator(new HeaderGenerator(bufferPool));
final List<DataFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onData(DataFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
@ -128,15 +127,15 @@ public class DataGenerateParseTest
DataGenerator generator = new DataGenerator(new HeaderGenerator(bufferPool));
final List<DataFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onData(DataFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
@ -130,15 +129,15 @@ public class FrameFloodTest
private void testFrameFlood(byte[] preamble, byte[] bytes)
{
AtomicBoolean failed = new AtomicBoolean();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192, new WindowRateControl(8, Duration.ofSeconds(1)));
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
{
failed.set(true);
}
}, 4096, 8192, new WindowRateControl(8, Duration.ofSeconds(1)));
parser.init(UnaryOperator.identity());
});
if (preamble != null)
{

View File

@ -17,7 +17,6 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.generator.GoAwayGenerator;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
@ -40,15 +39,15 @@ public class GoAwayGenerateParseTest
GoAwayGenerator generator = new GoAwayGenerator(new HeaderGenerator(bufferPool));
final List<GoAwayFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onGoAway(GoAwayFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int lastStreamId = 13;
int error = 17;
@ -82,15 +81,15 @@ public class GoAwayGenerateParseTest
GoAwayGenerator generator = new GoAwayGenerator(new HeaderGenerator(bufferPool));
final List<GoAwayFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onGoAway(GoAwayFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int lastStreamId = 13;
int error = 17;

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpField;
@ -52,15 +51,15 @@ public class HeadersGenerateParseTest
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP.asString(), new HostPortHttpField("localhost:8080"), "/path", HttpVersion.HTTP_2, fields, -1);
final List<HeadersFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
@ -105,15 +104,15 @@ public class HeadersGenerateParseTest
HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(bufferPool), new HpackEncoder());
final List<HeadersFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpFields;
@ -67,15 +66,15 @@ public class HeadersTooLargeParseTest
HeadersGenerator generator = new HeadersGenerator(new HeaderGenerator(bufferPool), new HpackEncoder());
AtomicInteger failure = new AtomicInteger();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, maxHeaderSize);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
{
failure.set(error);
}
}, 4096, maxHeaderSize);
parser.init(UnaryOperator.identity());
});
int streamId = 48;
ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.parser.Parser;
@ -32,26 +31,26 @@ public class MaxFrameSizeParseTest
@Test
public void testMaxFrameSize()
{
int maxFrameLength = Frame.DEFAULT_MAX_LENGTH + 16;
int maxFrameSize = Frame.DEFAULT_MAX_SIZE + 16;
AtomicInteger failure = new AtomicInteger();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.setMaxFrameSize(maxFrameSize);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
{
failure.set(error);
}
}, 4096, 8192);
parser.setMaxFrameLength(maxFrameLength);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure the parser is properly reset.
for (int i = 0; i < 2; ++i)
{
byte[] bytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0};
ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.putInt(0, maxFrameLength + 1);
buffer.putInt(0, maxFrameSize + 1);
buffer.position(1);
while (buffer.hasRemaining())
{

View File

@ -17,7 +17,6 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
import org.eclipse.jetty.http2.generator.PingGenerator;
@ -41,15 +40,15 @@ public class PingGenerateParseTest
PingGenerator generator = new PingGenerator(new HeaderGenerator(bufferPool));
final List<PingFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onPing(PingFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
byte[] payload = new byte[8];
new Random().nextBytes(payload);
@ -82,15 +81,15 @@ public class PingGenerateParseTest
PingGenerator generator = new PingGenerator(new HeaderGenerator(bufferPool));
final List<PingFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onPing(PingFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
byte[] payload = new byte[8];
new Random().nextBytes(payload);
@ -123,15 +122,15 @@ public class PingGenerateParseTest
PingGenerator generator = new PingGenerator(new HeaderGenerator(bufferPool));
final List<PingFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onPing(PingFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
PingFrame ping = new PingFrame(NanoTime.now(), true);

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
import org.eclipse.jetty.http2.generator.PriorityGenerator;
@ -37,15 +36,15 @@ public class PriorityGenerateParseTest
PriorityGenerator generator = new PriorityGenerator(new HeaderGenerator(bufferPool));
final List<PriorityFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onPriority(PriorityFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int parentStreamId = 17;
@ -82,15 +81,15 @@ public class PriorityGenerateParseTest
PriorityGenerator generator = new PriorityGenerator(new HeaderGenerator(bufferPool));
final List<PriorityFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onPriority(PriorityFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int parentStreamId = 17;

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpField;
@ -45,15 +44,15 @@ public class PushPromiseGenerateParseTest
PushPromiseGenerator generator = new PushPromiseGenerator(new HeaderGenerator(bufferPool), new HpackEncoder());
final List<PushPromiseFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onPushPromise(PushPromiseFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int promisedStreamId = 17;
@ -98,15 +97,15 @@ public class PushPromiseGenerateParseTest
PushPromiseGenerator generator = new PushPromiseGenerator(new HeaderGenerator(bufferPool), new HpackEncoder());
final List<PushPromiseFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onPushPromise(PushPromiseFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int promisedStreamId = 17;

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
import org.eclipse.jetty.http2.generator.ResetGenerator;
@ -37,15 +36,15 @@ public class ResetGenerateParseTest
ResetGenerator generator = new ResetGenerator(new HeaderGenerator(bufferPool));
final List<ResetFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onReset(ResetFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int error = 17;
@ -78,15 +77,15 @@ public class ResetGenerateParseTest
ResetGenerator generator = new ResetGenerator(new HeaderGenerator(bufferPool));
final List<ResetFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onReset(ResetFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int error = 17;

View File

@ -20,7 +20,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
@ -31,6 +30,7 @@ import org.eclipse.jetty.io.ByteBufferPool;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SettingsGenerateParseTest
@ -40,8 +40,7 @@ public class SettingsGenerateParseTest
@Test
public void testGenerateParseNoSettings()
{
List<SettingsFrame> frames = testGenerateParse(Collections.<Integer, Integer>emptyMap());
List<SettingsFrame> frames = testGenerateParse(Collections.emptyMap(), true);
assertEquals(1, frames.size());
SettingsFrame frame = frames.get(0);
assertEquals(0, frame.getSettings().size());
@ -58,7 +57,7 @@ public class SettingsGenerateParseTest
int key2 = 19;
Integer value2 = 23;
settings1.put(key2, value2);
List<SettingsFrame> frames = testGenerateParse(settings1);
List<SettingsFrame> frames = testGenerateParse(settings1, false);
assertEquals(1, frames.size());
SettingsFrame frame = frames.get(0);
Map<Integer, Integer> settings2 = frame.getSettings();
@ -67,26 +66,26 @@ public class SettingsGenerateParseTest
assertEquals(value2, settings2.get(key2));
}
private List<SettingsFrame> testGenerateParse(Map<Integer, Integer> settings)
private List<SettingsFrame> testGenerateParse(Map<Integer, Integer> settings, boolean reply)
{
SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator(bufferPool));
List<SettingsFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onSettings(SettingsFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure generator and parser are properly reset.
for (int i = 0; i < 2; ++i)
{
ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
generator.generateSettings(accumulator, settings, true);
generator.generateSettings(accumulator, settings, reply);
frames.clear();
for (ByteBuffer buffer : accumulator.getByteBuffers())
@ -107,20 +106,20 @@ public class SettingsGenerateParseTest
SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator(bufferPool));
AtomicInteger errorRef = new AtomicInteger();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
{
errorRef.set(error);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
Map<Integer, Integer> settings1 = new HashMap<>();
settings1.put(13, 17);
ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
generator.generateSettings(accumulator, settings1, true);
generator.generateSettings(accumulator, settings1, false);
// Modify the length of the frame to make it invalid
ByteBuffer bytes = accumulator.getByteBuffers().get(0);
bytes.putShort(1, (short)(bytes.getShort(1) - 1));
@ -142,15 +141,15 @@ public class SettingsGenerateParseTest
SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator(bufferPool));
List<SettingsFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onSettings(SettingsFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
Map<Integer, Integer> settings1 = new HashMap<>();
int key = 13;
@ -161,7 +160,7 @@ public class SettingsGenerateParseTest
for (int i = 0; i < 2; ++i)
{
ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
generator.generateSettings(accumulator, settings1, true);
generator.generateSettings(accumulator, settings1, false);
frames.clear();
for (ByteBuffer buffer : accumulator.getByteBuffers())
@ -177,7 +176,7 @@ public class SettingsGenerateParseTest
Map<Integer, Integer> settings2 = frame.getSettings();
assertEquals(1, settings2.size());
assertEquals(value, settings2.get(key));
assertTrue(frame.isReply());
assertFalse(frame.isReply());
}
}
@ -187,17 +186,17 @@ public class SettingsGenerateParseTest
SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator(bufferPool));
AtomicInteger errorRef = new AtomicInteger();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
int maxSettingsKeys = 32;
parser.setMaxSettingsKeys(maxSettingsKeys);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
{
errorRef.set(error);
}
}, 4096, 8192);
int maxSettingsKeys = 32;
parser.setMaxSettingsKeys(maxSettingsKeys);
parser.init(UnaryOperator.identity());
});
Map<Integer, Integer> settings = new HashMap<>();
for (int i = 0; i < maxSettingsKeys + 1; ++i)
@ -223,14 +222,14 @@ public class SettingsGenerateParseTest
public void testGenerateParseTooManySameSettingsInOneFrame() throws Exception
{
int keyValueLength = 6;
int pairs = Frame.DEFAULT_MAX_LENGTH / keyValueLength;
int pairs = Frame.DEFAULT_MAX_SIZE / keyValueLength;
int maxSettingsKeys = pairs / 2;
AtomicInteger errorRef = new AtomicInteger();
Parser parser = new Parser(bufferPool, new Parser.Listener() {}, 4096, 8192);
Parser parser = new Parser(bufferPool, 8192);
parser.setMaxSettingsKeys(maxSettingsKeys);
parser.setMaxFrameLength(Frame.DEFAULT_MAX_LENGTH);
parser.init(listener -> new Parser.Listener.Wrapper(listener)
parser.setMaxFrameSize(Frame.DEFAULT_MAX_SIZE);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
@ -268,17 +267,17 @@ public class SettingsGenerateParseTest
SettingsGenerator generator = new SettingsGenerator(new HeaderGenerator(bufferPool));
AtomicInteger errorRef = new AtomicInteger();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
int maxSettingsKeys = 32;
parser.setMaxSettingsKeys(maxSettingsKeys);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
{
errorRef.set(error);
}
}, 4096, 8192);
int maxSettingsKeys = 32;
parser.setMaxSettingsKeys(maxSettingsKeys);
parser.init(UnaryOperator.identity());
});
Map<Integer, Integer> settings = new HashMap<>();
settings.put(13, 17);

View File

@ -17,7 +17,6 @@ import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.parser.Parser;
@ -48,8 +47,8 @@ public class UnknownParseTest
public void testInvalidFrameSize()
{
AtomicInteger failure = new AtomicInteger();
Parser parser = new Parser(bufferPool, new Parser.Listener() {}, 4096, 8192);
parser.init(listener -> new Parser.Listener.Wrapper(listener)
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
@ -57,7 +56,7 @@ public class UnknownParseTest
failure.set(error);
}
});
parser.setMaxFrameLength(Frame.DEFAULT_MAX_LENGTH);
parser.setMaxFrameSize(Frame.DEFAULT_MAX_SIZE);
// 0x4001 == 16385 which is > Frame.DEFAULT_MAX_LENGTH.
byte[] bytes = new byte[]{0, 0x40, 0x01, 64, 0, 0, 0, 0, 0};
@ -73,15 +72,15 @@ public class UnknownParseTest
private void testParse(Function<ByteBuffer, ByteBuffer> fn)
{
AtomicBoolean failure = new AtomicBoolean();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onConnectionFailure(int error, String reason)
{
failure.set(true);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// Iterate a few times to be sure the parser is properly reset.
for (int i = 0; i < 2; ++i)

View File

@ -16,7 +16,6 @@ package org.eclipse.jetty.http2.frames;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http2.generator.HeaderGenerator;
import org.eclipse.jetty.http2.generator.WindowUpdateGenerator;
@ -37,15 +36,15 @@ public class WindowUpdateGenerateParseTest
WindowUpdateGenerator generator = new WindowUpdateGenerator(new HeaderGenerator(bufferPool));
final List<WindowUpdateFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onWindowUpdate(WindowUpdateFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int windowUpdate = 17;
@ -78,15 +77,15 @@ public class WindowUpdateGenerateParseTest
WindowUpdateGenerator generator = new WindowUpdateGenerator(new HeaderGenerator(bufferPool));
final List<WindowUpdateFrame> frames = new ArrayList<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onWindowUpdate(WindowUpdateFrame frame)
{
frames.add(frame);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
int streamId = 13;
int windowUpdate = 17;

View File

@ -115,6 +115,7 @@ public class HpackContext
private static final StaticEntry[] __staticTableByHeader = new StaticEntry[HttpHeader.values().length];
private static final StaticEntry[] __staticTable = new StaticEntry[STATIC_TABLE.length];
public static final int STATIC_SIZE = STATIC_TABLE.length - 1;
public static final int DEFAULT_MAX_TABLE_CAPACITY = 4096;
static
{
@ -184,26 +185,26 @@ public class HpackContext
}
}
private int _maxDynamicTableSizeInBytes;
private int _dynamicTableSizeInBytes;
private final DynamicTable _dynamicTable;
private final Map<HttpField, Entry> _fieldMap = new HashMap<>();
private final Map<String, Entry> _nameMap = new HashMap<>();
private int _maxTableSize;
private int _tableSize;
HpackContext(int maxDynamicTableSize)
HpackContext(int maxTableSize)
{
_maxDynamicTableSizeInBytes = maxDynamicTableSize;
int guesstimateEntries = 10 + maxDynamicTableSize / (32 + 10 + 10);
_maxTableSize = maxTableSize;
int guesstimateEntries = 10 + maxTableSize / (32 + 10 + 10);
_dynamicTable = new DynamicTable(guesstimateEntries);
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] created max=%d", hashCode(), maxDynamicTableSize));
LOG.debug(String.format("HdrTbl[%x] created max=%d", hashCode(), maxTableSize));
}
public void resize(int newMaxDynamicTableSize)
{
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d", hashCode(), _maxDynamicTableSizeInBytes, newMaxDynamicTableSize));
_maxDynamicTableSizeInBytes = newMaxDynamicTableSize;
LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d", hashCode(), _maxTableSize, newMaxDynamicTableSize));
_maxTableSize = newMaxDynamicTableSize;
_dynamicTable.evict();
}
@ -248,14 +249,14 @@ public class HpackContext
{
Entry entry = new Entry(field);
int size = entry.getSize();
if (size > _maxDynamicTableSizeInBytes)
if (size > _maxTableSize)
{
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] !added size %d>%d", hashCode(), size, _maxDynamicTableSizeInBytes));
LOG.debug(String.format("HdrTbl[%x] !added size %d>%d", hashCode(), size, _maxTableSize));
_dynamicTable.evictAll();
return null;
}
_dynamicTableSizeInBytes += size;
_tableSize += size;
_dynamicTable.add(entry);
_fieldMap.put(field, entry);
_nameMap.put(field.getLowerCaseName(), entry);
@ -279,7 +280,7 @@ public class HpackContext
*/
public int getDynamicTableSize()
{
return _dynamicTableSizeInBytes;
return _tableSize;
}
/**
@ -287,7 +288,7 @@ public class HpackContext
*/
public int getMaxDynamicTableSize()
{
return _maxDynamicTableSizeInBytes;
return _maxTableSize;
}
public int index(Entry entry)
@ -313,15 +314,15 @@ public class HpackContext
@Override
public String toString()
{
return String.format("HpackContext@%x{entries=%d,size=%d,max=%d}", hashCode(), _dynamicTable.size(), _dynamicTableSizeInBytes, _maxDynamicTableSizeInBytes);
return String.format("HpackContext@%x{entries=%d,size=%d,max=%d}", hashCode(), _dynamicTable.size(), _tableSize, _maxTableSize);
}
private class DynamicTable
{
Entry[] _entries;
int _size;
int _offset;
int _growby;
private Entry[] _entries;
private final int _growby;
private int _size;
private int _offset;
private DynamicTable(int initCapacity)
{
@ -369,7 +370,7 @@ public class HpackContext
private void evict()
{
while (_dynamicTableSizeInBytes > _maxDynamicTableSizeInBytes)
while (_tableSize > _maxTableSize)
{
Entry entry = _entries[_offset];
_entries[_offset] = null;
@ -377,7 +378,7 @@ public class HpackContext
_size--;
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] evict %s", HpackContext.this.hashCode(), entry));
_dynamicTableSizeInBytes -= entry.getSize();
_tableSize -= entry.getSize();
entry._slot = -1;
_fieldMap.remove(entry.getHttpField());
String lc = entry.getHttpField().getLowerCaseName();
@ -385,7 +386,7 @@ public class HpackContext
_nameMap.remove(lc);
}
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] entries=%d, size=%d, max=%d", HpackContext.this.hashCode(), _dynamicTable.size(), _dynamicTableSizeInBytes, _maxDynamicTableSizeInBytes));
LOG.debug(String.format("HdrTbl[%x] entries=%d, size=%d, max=%d", HpackContext.this.hashCode(), _dynamicTable.size(), _tableSize, _maxTableSize));
}
private void evictAll()
@ -398,7 +399,7 @@ public class HpackContext
_nameMap.clear();
_offset = 0;
_size = 0;
_dynamicTableSizeInBytes = 0;
_tableSize = 0;
Arrays.fill(_entries, null);
}
}

View File

@ -43,19 +43,19 @@ public class HpackDecoder
private final MetaDataBuilder _builder;
private final HuffmanDecoder _huffmanDecoder;
private final NBitIntegerDecoder _integerDecoder;
private int _localMaxDynamicTableSize;
private int _maxTableCapacity;
/**
* @param localMaxDynamicTableSize The maximum allowed size of the local dynamic header field table.
* @param maxHeaderSize The maximum allowed size of a headers block, expressed as total of all name and value characters, plus 32 per field
* @param maxHeaderSize The maximum allowed size of a decoded headers block,
* expressed as total of all name and value bytes, plus 32 bytes per field
*/
public HpackDecoder(int localMaxDynamicTableSize, int maxHeaderSize)
public HpackDecoder(int maxHeaderSize)
{
_context = new HpackContext(localMaxDynamicTableSize);
_localMaxDynamicTableSize = localMaxDynamicTableSize;
_context = new HpackContext(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
_builder = new MetaDataBuilder(maxHeaderSize);
_huffmanDecoder = new HuffmanDecoder();
_integerDecoder = new NBitIntegerDecoder();
setMaxTableCapacity(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
}
public HpackContext getHpackContext()
@ -63,9 +63,29 @@ public class HpackDecoder
return _context;
}
public void setLocalMaxDynamicTableSize(int localMaxdynamciTableSize)
public int getMaxTableCapacity()
{
_localMaxDynamicTableSize = localMaxdynamciTableSize;
return _maxTableCapacity;
}
/**
* <p>Sets the limit for the capacity of the dynamic header table.</p>
* <p>This value acts as a limit for the values received from the
* remote peer via the HPACK dynamic table size update instruction.</p>
* <p>After calling this method, a SETTINGS frame must be sent to the other
* peer, containing the {@code SETTINGS_HEADER_TABLE_SIZE} setting with
* the value passed as argument to this method.</p>
*
* @param maxTableCapacity the limit for capacity of the dynamic header table
*/
public void setMaxTableCapacity(int maxTableCapacity)
{
_maxTableCapacity = maxTableCapacity;
}
public void setMaxHeaderListSize(int maxHeaderListSize)
{
_builder.setMaxSize(maxHeaderListSize);
}
public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException, HpackException.StreamException
@ -73,13 +93,12 @@ public class HpackDecoder
if (LOG.isDebugEnabled())
LOG.debug(String.format("CtxTbl[%x] decoding %d octets", _context.hashCode(), buffer.remaining()));
// If the buffer is big, don't even think about decoding it.
// Huffman may double the size, but it will only be a temporary allocation until detected in MetaDataBuilder.emit().
if (buffer.remaining() > _builder.getMaxSize())
throw new HpackException.SessionException("431 Request Header Fields too large");
// If the buffer is larger than the max headers size, don't even start decoding it.
int maxSize = _builder.getMaxSize();
if (maxSize > 0 && buffer.remaining() > maxSize)
throw new HpackException.SessionException("Header fields size too large");
boolean emitted = false;
while (buffer.hasRemaining())
{
if (LOG.isDebugEnabled())
@ -133,8 +152,8 @@ public class HpackDecoder
int size = integerDecode(buffer, 5);
if (LOG.isDebugEnabled())
LOG.debug("decode resize={}", size);
if (size > _localMaxDynamicTableSize)
throw new IllegalArgumentException();
if (size > getMaxTableCapacity())
throw new HpackException.CompressionException("Dynamic table resize exceeded max limit");
if (emitted)
throw new HpackException.CompressionException("Dynamic table resize after fields");
_context.resize(size);

View File

@ -94,34 +94,57 @@ public class HpackEncoder
private final HpackContext _context;
private final boolean _debug;
private int _remoteMaxDynamicTableSize;
private int _localMaxDynamicTableSize;
private int _maxTableCapacity;
private int _tableCapacity;
private int _maxHeaderListSize;
private int _headerListSize;
private boolean _validateEncoding = true;
public HpackEncoder()
{
this(4096, 4096, -1);
}
public HpackEncoder(int localMaxDynamicTableSize)
{
this(localMaxDynamicTableSize, 4096, -1);
}
public HpackEncoder(int localMaxDynamicTableSize, int remoteMaxDynamicTableSize)
{
this(localMaxDynamicTableSize, remoteMaxDynamicTableSize, -1);
}
public HpackEncoder(int localMaxDynamicTableSize, int remoteMaxDynamicTableSize, int maxHeaderListSize)
{
_context = new HpackContext(remoteMaxDynamicTableSize);
_remoteMaxDynamicTableSize = remoteMaxDynamicTableSize;
_localMaxDynamicTableSize = localMaxDynamicTableSize;
_maxHeaderListSize = maxHeaderListSize;
_context = new HpackContext(0);
_debug = LOG.isDebugEnabled();
setMaxTableCapacity(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
setTableCapacity(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
}
public int getMaxTableCapacity()
{
return _maxTableCapacity;
}
/**
* <p>Sets the limit for the capacity of the dynamic header table.</p>
* <p>This value is set by the remote peer via the
* {@code SETTINGS_HEADER_TABLE_SIZE} setting.</p>
*
* @param maxTableSizeLimit the limit for capacity of the dynamic header table
*/
public void setMaxTableCapacity(int maxTableSizeLimit)
{
_maxTableCapacity = maxTableSizeLimit;
}
public int getTableCapacity()
{
return _tableCapacity;
}
/**
* <p>Sets the capacity of the dynamic header table.</p>
* <p>The value of the capacity may be changed from {@code 0}
* up to {@link #getMaxTableCapacity()}.
* An HPACK instruction with the new capacity value will
* be sent to the decoder when the next call to
* {@link #encode(ByteBuffer, MetaData)} is made.</p>
*
* @param tableCapacity the capacity of the dynamic header table
*/
public void setTableCapacity(int tableCapacity)
{
if (tableCapacity > getMaxTableCapacity())
throw new IllegalArgumentException("Max table capacity exceeded");
_tableCapacity = tableCapacity;
}
public int getMaxHeaderListSize()
@ -139,16 +162,6 @@ public class HpackEncoder
return _context;
}
public void setRemoteMaxDynamicTableSize(int remoteMaxDynamicTableSize)
{
_remoteMaxDynamicTableSize = remoteMaxDynamicTableSize;
}
public void setLocalMaxDynamicTableSize(int localMaxDynamicTableSize)
{
_localMaxDynamicTableSize = localMaxDynamicTableSize;
}
public boolean isValidateEncoding()
{
return _validateEncoding;
@ -182,10 +195,10 @@ public class HpackEncoder
_headerListSize = 0;
int pos = buffer.position();
// Check the dynamic table sizes!
int maxDynamicTableSize = Math.min(_remoteMaxDynamicTableSize, _localMaxDynamicTableSize);
if (maxDynamicTableSize != _context.getMaxDynamicTableSize())
encodeMaxDynamicTableSize(buffer, maxDynamicTableSize);
// If max table size changed, send the correspondent instruction.
int tableCapacity = getTableCapacity();
if (tableCapacity != _context.getMaxDynamicTableSize())
encodeMaxDynamicTableSize(buffer, tableCapacity);
// Add Request/response meta fields
if (metadata.isRequest())
@ -261,14 +274,9 @@ public class HpackEncoder
}
}
// Check size
if (_maxHeaderListSize > 0 && _headerListSize > _maxHeaderListSize)
{
if (LOG.isDebugEnabled())
LOG.warn("Header list size too large {} > {} metadata={}", _headerListSize, _maxHeaderListSize, metadata);
else
LOG.warn("Header list size too large {} > {}", _headerListSize, _maxHeaderListSize);
}
int maxHeaderListSize = getMaxHeaderListSize();
if (maxHeaderListSize > 0 && _headerListSize > maxHeaderListSize)
throw new HpackException.SessionException("Header size %d > %d", _headerListSize, maxHeaderListSize);
if (LOG.isDebugEnabled())
LOG.debug(String.format("CtxTbl[%x] encoded %d octets", _context.hashCode(), buffer.position() - pos));
@ -285,13 +293,11 @@ public class HpackEncoder
}
}
public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize)
public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxTableSize)
{
if (maxDynamicTableSize > _remoteMaxDynamicTableSize)
throw new IllegalArgumentException();
buffer.put((byte)0x20);
NBitIntegerEncoder.encode(buffer, 5, maxDynamicTableSize);
_context.resize(maxDynamicTableSize);
NBitIntegerEncoder.encode(buffer, 5, maxTableSize);
_context.resize(maxTableSize);
}
public void encode(ByteBuffer buffer, HttpField field)

View File

@ -26,8 +26,8 @@ import org.eclipse.jetty.http2.hpack.HpackException.SessionException;
public class MetaDataBuilder
{
private final int _maxSize;
private final HttpFields.Mutable _fields = HttpFields.build();
private int _maxSize;
private int _size;
private Integer _status;
private String _method;
@ -49,8 +49,6 @@ public class MetaDataBuilder
}
/**
* Get the maxSize.
*
* @return the maxSize
*/
public int getMaxSize()
@ -58,6 +56,11 @@ public class MetaDataBuilder
return _maxSize;
}
public void setMaxSize(int maxSize)
{
_maxSize = maxSize;
}
/**
* Get the size.
*
@ -77,8 +80,9 @@ public class MetaDataBuilder
String value = field.getValue();
int fieldSize = name.length() + (value == null ? 0 : value.length());
_size += fieldSize + 32;
if (_size > _maxSize)
throw new SessionException("Header size %d > %d", _size, _maxSize);
int maxSize = getMaxSize();
if (maxSize > 0 && _size > maxSize)
throw new SessionException("Header size %d > %d", _size, maxSize);
if (field instanceof StaticTableHttpField staticField)
{

View File

@ -58,7 +58,7 @@ public class HpackDecoderTest
@Test
public void testDecodeD3() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
// First request
String encoded = "828684410f7777772e6578616d706c652e636f6d";
@ -106,7 +106,7 @@ public class HpackDecoderTest
@Test
public void testDecodeD4() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
// First request
String encoded = "828684418cf1e3c2e5f23a6ba0ab90f4ff";
@ -141,7 +141,7 @@ public class HpackDecoderTest
{
String value = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "8682418cF1E3C2E5F23a6bA0Ab90F4Ff841f0822426173696320515778685a475270626a70766347567549484e6c633246745a513d3d";
byte[] bytes = StringUtil.fromHexString(encoded);
byte[] array = new byte[bytes.length + 1];
@ -163,7 +163,7 @@ public class HpackDecoderTest
@Test
public void testDecodeHuffmanWithArrayOffset() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "8286418cf1e3c2e5f23a6ba0ab90f4ff84";
byte[] bytes = StringUtil.fromHexString(encoded);
@ -187,7 +187,7 @@ public class HpackDecoderTest
String encoded = "886196C361Be940b6a65B6850400B8A00571972e080a62D1Bf5f87497cA589D34d1f9a0f0d0234327690Aa69D29aFcA954D3A5358980Ae112e0f7c880aE152A9A74a6bF3";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
MetaData.Response response = (MetaData.Response)decoder.decode(buffer);
assertThat(response.getStatus(), is(200));
@ -205,7 +205,7 @@ public class HpackDecoderTest
{
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
MetaData metaData = decoder.decode(buffer);
assertThat(metaData.getHttpFields().get(HttpHeader.HOST), is("localhost0"));
assertThat(metaData.getHttpFields().get(HttpHeader.COOKIE), is("abcdefghij"));
@ -227,7 +227,7 @@ public class HpackDecoderTest
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f20";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
try
{
decoder.decode(buffer);
@ -245,7 +245,8 @@ public class HpackDecoderTest
String encoded = "3f610f17FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(128, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
decoder.setMaxTableCapacity(128);
MetaData metaData = decoder.decode(buffer);
assertThat(decoder.getHpackContext().getDynamicTableSize(), is(0));
@ -258,7 +259,8 @@ public class HpackDecoderTest
String encoded = "BE";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
HpackDecoder decoder = new HpackDecoder(128, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
decoder.setMaxTableCapacity(128);
try
{
@ -443,7 +445,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedStandard() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "82868441" + "83" + "49509F";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -461,7 +463,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedExtraPadding()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "82868441" + "84" + "49509FFF";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -473,7 +475,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedZeroPadding()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "82868441" + "83" + "495090";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -486,7 +488,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedWithEOS()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "82868441" + "87" + "497FFFFFFF427F";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -498,7 +500,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedOneIncompleteOctet()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "82868441" + "81" + "FE";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -510,7 +512,7 @@ public class HpackDecoderTest
@Test
public void testHuffmanEncodedTwoIncompleteOctet()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "82868441" + "82" + "FFFE";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -522,7 +524,7 @@ public class HpackDecoderTest
@Test
public void testZeroLengthName()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "00000130";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -533,7 +535,7 @@ public class HpackDecoderTest
@Test
public void testZeroLengthValue() throws Exception
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "00016800";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -545,7 +547,7 @@ public class HpackDecoderTest
@Test
public void testUpperCaseName()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "0001480130";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
@ -556,7 +558,7 @@ public class HpackDecoderTest
@Test
public void testWhiteSpaceName()
{
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
String encoded = "0001200130";
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(encoded));

View File

@ -33,7 +33,7 @@ public class HpackEncoderTest
@Test
public void testUnknownFieldsContextManagement() throws Exception
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
HpackEncoder encoder = newHpackEncoder(38 * 5);
HttpFields.Mutable fields = HttpFields.build();
HttpField[] field =
@ -144,8 +144,9 @@ public class HpackEncoderTest
@Test
public void testLargeFieldsNotIndexed()
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
HpackEncoder encoder = newHpackEncoder(38 * 5);
HpackContext ctx = encoder.getHpackContext();
ctx.resize(encoder.getMaxTableCapacity());
ByteBuffer buffer = BufferUtil.allocate(4096);
@ -170,8 +171,9 @@ public class HpackEncoderTest
@Test
public void testIndexContentLength()
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
HpackEncoder encoder = newHpackEncoder(38 * 5);
HpackContext ctx = encoder.getHpackContext();
ctx.resize(encoder.getMaxTableCapacity());
ByteBuffer buffer = BufferUtil.allocate(4096);
@ -192,7 +194,7 @@ public class HpackEncoderTest
@Test
public void testNeverIndexSetCookie() throws Exception
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
HpackEncoder encoder = newHpackEncoder(38 * 5);
ByteBuffer buffer = BufferUtil.allocate(4096);
HttpFields.Mutable fields = HttpFields.build()
@ -226,20 +228,20 @@ public class HpackEncoderTest
{
HttpFields.Mutable fields = HttpFields.build();
HpackEncoder encoder = new HpackEncoder(128);
HpackEncoder encoder = newHpackEncoder(128);
ByteBuffer buffer0 = BufferUtil.allocate(4096);
int pos = BufferUtil.flipToFill(buffer0);
encoder.encode(buffer0, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer0, pos);
encoder = new HpackEncoder(128);
encoder = newHpackEncoder(128);
fields.add(new HttpField("user-agent", "jetty/test"));
ByteBuffer buffer1 = BufferUtil.allocate(4096);
pos = BufferUtil.flipToFill(buffer1);
encoder.encode(buffer1, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer1, pos);
encoder = new HpackEncoder(128);
encoder = newHpackEncoder(128);
encoder.setValidateEncoding(false);
fields.add(new HttpField(":path",
"This is a very large field, whose size is larger than the dynamic table so it should not be indexed as it will not fit in the table ever!" +
@ -251,7 +253,7 @@ public class HpackEncoderTest
encoder.encode(buffer2, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer2, pos);
encoder = new HpackEncoder(128);
encoder = newHpackEncoder(128);
encoder.setValidateEncoding(false);
fields.add(new HttpField("host", "somehost"));
ByteBuffer buffer = BufferUtil.allocate(4096);
@ -292,12 +294,12 @@ public class HpackEncoderTest
.add("host", "localhost0")
.add("cookie", "abcdefghij");
HpackEncoder encoder = new HpackEncoder(4096);
HpackEncoder encoder = newHpackEncoder(4096);
ByteBuffer buffer = BufferUtil.allocate(4096);
int pos = BufferUtil.flipToFill(buffer);
encoder.encodeMaxDynamicTableSize(buffer, 0);
encoder.setRemoteMaxDynamicTableSize(50);
encoder.setTableCapacity(50);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, pos);
@ -306,4 +308,12 @@ public class HpackEncoderTest
assertThat(context.getMaxDynamicTableSize(), Matchers.is(50));
assertThat(context.size(), Matchers.is(1));
}
private static HpackEncoder newHpackEncoder(int tableCapacity)
{
HpackEncoder encoder = new HpackEncoder();
encoder.setMaxTableCapacity(tableCapacity);
encoder.setTableCapacity(tableCapacity);
return encoder;
}
}

View File

@ -32,7 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
public class HpackPerfTest
{
int _maxDynamicTableSize = 4 * 1024;
int _tableCapacity = 4 * 1024;
int _unencodedSize;
int _encodedSize;
@ -46,7 +46,7 @@ public class HpackPerfTest
@AfterEach
public void after()
{
System.err.printf("dynamictable=%d unencoded=%d encoded=%d p=%3.1f%%%n", _maxDynamicTableSize, _unencodedSize, _encodedSize, 100.0 * _encodedSize / _unencodedSize);
System.err.printf("dynamictable=%d unencoded=%d encoded=%d p=%3.1f%%%n", _tableCapacity, _unencodedSize, _encodedSize, 100.0 * _encodedSize / _unencodedSize);
}
@Test
@ -92,7 +92,9 @@ public class HpackPerfTest
{
if (type.equals(story.get("context")))
{
HpackEncoder encoder = new HpackEncoder(_maxDynamicTableSize, _maxDynamicTableSize);
HpackEncoder encoder = new HpackEncoder();
encoder.setMaxTableCapacity(_tableCapacity);
encoder.setTableCapacity(_tableCapacity);
encoder.setValidateEncoding(false);
Object[] cases = (Object[])story.get("cases");

View File

@ -43,7 +43,7 @@ public class HpackTest
public void encodeDecodeResponseTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 8192);
HpackDecoder decoder = new HpackDecoder(8192);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
long contentLength = 1024;
@ -99,7 +99,7 @@ public class HpackTest
public void encodeDecodeTooLargeTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 164);
HpackDecoder decoder = new HpackDecoder(164);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
HttpFields fields0 = HttpFields.build()
@ -159,8 +159,11 @@ public class HpackTest
@Test
public void evictReferencedFieldTest() throws Exception
{
HpackEncoder encoder = new HpackEncoder(200, 200);
HpackDecoder decoder = new HpackDecoder(200, 1024);
HpackDecoder decoder = new HpackDecoder(1024);
decoder.setMaxTableCapacity(200);
HpackEncoder encoder = new HpackEncoder();
encoder.setMaxTableCapacity(decoder.getMaxTableCapacity());
encoder.setTableCapacity(decoder.getMaxTableCapacity());
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
String longEnoughToBeEvicted = "012345678901234567890123456789012345678901234567890";
@ -203,7 +206,7 @@ public class HpackTest
public void testHopHeadersAreRemoved() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 16384);
HpackDecoder decoder = new HpackDecoder(16384);
HttpFields input = HttpFields.build()
.add(HttpHeader.ACCEPT, "*")
@ -230,7 +233,7 @@ public class HpackTest
public void testTETrailers() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 16384);
HpackDecoder decoder = new HpackDecoder(16384);
String teValue = "trailers";
String trailerValue = "Custom";
@ -255,7 +258,7 @@ public class HpackTest
public void testColonHeaders() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 16384);
HpackDecoder decoder = new HpackDecoder(16384);
HttpFields input = HttpFields.build()
.add(":status", "200")

View File

@ -34,10 +34,10 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.hpack.HpackContext;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.http2.server.internal.HTTP2ServerConnection;
import org.eclipse.jetty.http2.server.internal.HTTP2ServerSession;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.AbstractConnectionFactory;
@ -64,12 +64,13 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
private final HTTP2SessionContainer sessionContainer = new HTTP2SessionContainer();
private final HttpConfiguration httpConfiguration;
private int maxDynamicTableSize = 4096;
private int maxDecoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxEncoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int initialSessionRecvWindow = 1024 * 1024;
private int initialStreamRecvWindow = 512 * 1024;
private int maxConcurrentStreams = 128;
private int maxHeaderBlockFragment = 0;
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
private int maxFrameSize = Frame.DEFAULT_MAX_SIZE;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private boolean connectProtocolEnabled = true;
private RateControl.Factory rateControlFactory = new WindowRateControl.Factory(50);
@ -94,20 +95,37 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
addBean(sessionContainer);
this.httpConfiguration = Objects.requireNonNull(httpConfiguration);
addBean(httpConfiguration);
setInputBufferSize(Frame.DEFAULT_MAX_LENGTH + Frame.HEADER_LENGTH);
setInputBufferSize(Frame.DEFAULT_MAX_SIZE + Frame.HEADER_LENGTH);
setUseInputDirectByteBuffers(httpConfiguration.isUseInputDirectByteBuffers());
setUseOutputDirectByteBuffers(httpConfiguration.isUseOutputDirectByteBuffers());
}
@ManagedAttribute("The HPACK dynamic table maximum size")
public int getMaxDynamicTableSize()
@ManagedAttribute("The HPACK encoder dynamic table maximum capacity")
public int getMaxEncoderTableCapacity()
{
return maxDynamicTableSize;
return maxEncoderTableCapacity;
}
public void setMaxDynamicTableSize(int maxDynamicTableSize)
/**
* <p>Sets the limit for the encoder HPACK dynamic table capacity.</p>
* <p>Setting this value to {@code 0} disables the use of the dynamic table.</p>
*
* @param maxEncoderTableCapacity The HPACK encoder dynamic table maximum capacity
*/
public void setMaxEncoderTableCapacity(int maxEncoderTableCapacity)
{
this.maxDynamicTableSize = maxDynamicTableSize;
this.maxEncoderTableCapacity = maxEncoderTableCapacity;
}
@ManagedAttribute("The HPACK decoder dynamic table maximum capacity")
public int getMaxDecoderTableCapacity()
{
return maxDecoderTableCapacity;
}
public void setMaxDecoderTableCapacity(int maxDecoderTableCapacity)
{
this.maxDecoderTableCapacity = maxDecoderTableCapacity;
}
@ManagedAttribute("The initial size of session's flow control receive window")
@ -175,15 +193,15 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
this.streamIdleTimeout = streamIdleTimeout;
}
@ManagedAttribute("The max frame length in bytes")
public int getMaxFrameLength()
@ManagedAttribute("The max frame size in bytes")
public int getMaxFrameSize()
{
return maxFrameLength;
return maxFrameSize;
}
public void setMaxFrameLength(int maxFrameLength)
public void setMaxFrameSize(int maxFrameSize)
{
this.maxFrameLength = maxFrameLength;
this.maxFrameSize = maxFrameSize;
}
@ManagedAttribute("The max number of keys in all SETTINGS frames")
@ -256,12 +274,16 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
protected Map<Integer, Integer> newSettings()
{
Map<Integer, Integer> settings = new HashMap<>();
settings.put(SettingsFrame.HEADER_TABLE_SIZE, getMaxDynamicTableSize());
settings.put(SettingsFrame.INITIAL_WINDOW_SIZE, getInitialStreamRecvWindow());
int maxConcurrentStreams = getMaxConcurrentStreams();
if (maxConcurrentStreams >= 0)
settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, maxConcurrentStreams);
settings.put(SettingsFrame.MAX_HEADER_LIST_SIZE, getHttpConfiguration().getRequestHeaderSize());
int maxTableSize = getMaxDecoderTableCapacity();
if (maxTableSize != HpackContext.DEFAULT_MAX_TABLE_CAPACITY)
settings.put(SettingsFrame.HEADER_TABLE_SIZE, maxTableSize);
int initialStreamRecvWindow = getInitialStreamRecvWindow();
if (initialStreamRecvWindow != FlowControlStrategy.DEFAULT_WINDOW_SIZE)
settings.put(SettingsFrame.INITIAL_WINDOW_SIZE, initialStreamRecvWindow);
settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, getMaxConcurrentStreams());
int maxHeadersSize = getHttpConfiguration().getRequestHeaderSize();
if (maxHeadersSize > 0)
settings.put(SettingsFrame.MAX_HEADER_LIST_SIZE, maxHeadersSize);
settings.put(SettingsFrame.ENABLE_CONNECT_PROTOCOL, isConnectProtocolEnabled() ? 1 : 0);
return settings;
}
@ -271,11 +293,17 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
{
ServerSessionListener listener = newSessionListener(connector, endPoint);
Generator generator = new Generator(connector.getByteBufferPool(), isUseOutputDirectByteBuffers(), getMaxDynamicTableSize(), getMaxHeaderBlockFragment());
Generator generator = new Generator(connector.getByteBufferPool(), isUseOutputDirectByteBuffers(), getMaxHeaderBlockFragment());
FlowControlStrategy flowControl = getFlowControlStrategyFactory().newFlowControlStrategy();
HTTP2ServerSession session = new HTTP2ServerSession(connector.getScheduler(), endPoint, generator, listener, flowControl);
ServerParser parser = newServerParser(connector, getRateControlFactory().newRateControl(endPoint));
parser.setMaxFrameSize(getMaxFrameSize());
parser.setMaxSettingsKeys(getMaxSettingsKeys());
HTTP2ServerSession session = new HTTP2ServerSession(connector.getScheduler(), endPoint, parser, generator, listener, flowControl);
session.setMaxLocalStreams(getMaxConcurrentStreams());
session.setMaxRemoteStreams(getMaxConcurrentStreams());
session.setMaxEncoderTableCapacity(getMaxEncoderTableCapacity());
// For a single stream in a connection, there will be a race between
// the stream idle timeout and the connection idle timeout. However,
// the typical case is that the connection will be busier and the
@ -287,25 +315,21 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize());
session.setConnectProtocolEnabled(isConnectProtocolEnabled());
ServerParser parser = newServerParser(connector, session, getRateControlFactory().newRateControl(endPoint));
parser.setMaxFrameLength(getMaxFrameLength());
parser.setMaxSettingsKeys(getMaxSettingsKeys());
ByteBufferPool byteBufferPool = connector.getByteBufferPool();
HTTP2Connection connection = new HTTP2ServerConnection(byteBufferPool, connector,
endPoint, httpConfiguration, parser, session, getInputBufferSize(), listener);
HTTP2Connection connection = new HTTP2ServerConnection(connector,
endPoint, httpConfiguration, session, getInputBufferSize(), listener);
connection.setUseInputDirectByteBuffers(isUseInputDirectByteBuffers());
connection.setUseOutputDirectByteBuffers(isUseOutputDirectByteBuffers());
connection.addEventListener(sessionContainer);
parser.init(connection);
return configure(connection, connector, endPoint);
}
protected abstract ServerSessionListener newSessionListener(Connector connector, EndPoint endPoint);
private ServerParser newServerParser(Connector connector, ServerParser.Listener listener, RateControl rateControl)
private ServerParser newServerParser(Connector connector, RateControl rateControl)
{
return new ServerParser(connector.getByteBufferPool(), listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize(), rateControl);
return new ServerParser(connector.getByteBufferPool(), getHttpConfiguration().getRequestHeaderSize(), rateControl);
}
@ManagedObject("The container of HTTP/2 sessions")

View File

@ -40,7 +40,6 @@ import org.eclipse.jetty.http2.frames.PrefaceFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.http2.parser.SettingsBodyParser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ssl.SslConnection;
@ -57,7 +56,7 @@ import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HTTP2ServerConnection extends HTTP2Connection implements ConnectionMetaData
public class HTTP2ServerConnection extends HTTP2Connection implements ConnectionMetaData, ServerParser.Listener
{
private static final Logger LOG = LoggerFactory.getLogger(HTTP2ServerConnection.class);
@ -69,9 +68,9 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
private final HttpConfiguration httpConfig;
private final String id;
public HTTP2ServerConnection(ByteBufferPool byteBufferPool, Connector connector, EndPoint endPoint, HttpConfiguration httpConfig, ServerParser parser, HTTP2Session session, int inputBufferSize, ServerSessionListener listener)
public HTTP2ServerConnection(Connector connector, EndPoint endPoint, HttpConfiguration httpConfig, HTTP2ServerSession session, int inputBufferSize, ServerSessionListener listener)
{
super(byteBufferPool, connector.getExecutor(), endPoint, parser, session, inputBufferSize);
super(connector.getByteBufferPool(), connector.getExecutor(), endPoint, session, inputBufferSize);
this.connector = connector;
this.listener = listener;
this.httpConfig = httpConfig;
@ -79,9 +78,9 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
}
@Override
protected ServerParser getParser()
public HTTP2ServerSession getSession()
{
return (ServerParser)super.getParser();
return (HTTP2ServerSession)super.getSession();
}
@Override
@ -109,6 +108,12 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
}
}
@Override
public void onPreface()
{
getSession().onPreface();
}
public void onNewStream(HTTP2Stream stream, HeadersFrame frame)
{
if (LOG.isDebugEnabled())
@ -301,7 +306,7 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
{
if (HttpMethod.PRI.is(request.getMethod()))
{
getParser().directUpgrade();
getSession().directUpgrade();
}
else
{
@ -324,7 +329,7 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
responseFields.put(HttpHeader.UPGRADE, "h2c");
responseFields.put(HttpHeader.CONNECTION, "Upgrade");
getParser().standardUpgrade();
getSession().standardUpgrade();
// We fake that we received a client preface, so that we can send the
// server preface as the first HTTP/2 frame as required by the spec.

View File

@ -46,12 +46,18 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
private final ServerSessionListener listener;
public HTTP2ServerSession(Scheduler scheduler, EndPoint endPoint, Generator generator, ServerSessionListener listener, FlowControlStrategy flowControl)
public HTTP2ServerSession(Scheduler scheduler, EndPoint endPoint, ServerParser parser, Generator generator, ServerSessionListener listener, FlowControlStrategy flowControl)
{
super(scheduler, endPoint, generator, listener, flowControl, 2);
super(scheduler, endPoint, parser, generator, listener, flowControl, 2);
this.listener = listener;
}
@Override
public ServerParser getParser()
{
return (ServerParser)super.getParser();
}
@Override
public void onPreface()
{
@ -175,4 +181,14 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
default -> super.onFrame(frame);
}
}
public void directUpgrade()
{
getParser().directUpgrade();
}
public void standardUpgrade()
{
getParser().standardUpgrade();
}
}

View File

@ -22,7 +22,6 @@ import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpVersion;
@ -88,7 +87,8 @@ public class CloseTest extends AbstractServerTest
output.write(BufferUtil.toArray(buffer));
}
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
@ -105,8 +105,7 @@ public class CloseTest extends AbstractServerTest
throw new RuntimeIOException(x);
}
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -153,7 +152,8 @@ public class CloseTest extends AbstractServerTest
// Don't close the connection; the server should close.
final CountDownLatch responseLatch = new CountDownLatch(1);
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
@ -162,8 +162,7 @@ public class CloseTest extends AbstractServerTest
// HEADERS, the server is able to send us the response.
responseLatch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -218,7 +217,8 @@ public class CloseTest extends AbstractServerTest
final CountDownLatch responseLatch = new CountDownLatch(1);
final CountDownLatch closeLatch = new CountDownLatch(1);
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
@ -231,8 +231,7 @@ public class CloseTest extends AbstractServerTest
{
closeLatch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);

View File

@ -98,6 +98,7 @@ public class ConcurrentStreamCreationTest extends AbstractTest
x.printStackTrace();
}
}).start());
assertTrue(clientLatch.await(total, TimeUnit.MILLISECONDS), String.format("Missing streams on client: %d/%d", clientLatch.getCount(), total));
assertTrue(serverLatch.await(total, TimeUnit.MILLISECONDS), String.format("Missing streams on server: %d/%d", serverLatch.getCount(), total));
assertTrue(responseLatch.await(total, TimeUnit.MILLISECONDS), String.format("Missing response on client: %d/%d", clientLatch.getCount(), total));

View File

@ -35,6 +35,7 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.AbstractFlowControlStrategy;
import org.eclipse.jetty.http2.BufferingFlowControlStrategy;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.FlowControlStrategy;
@ -63,6 +64,7 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -203,11 +205,9 @@ public abstract class FlowControlStrategyTest
SettingsFrame frame = new SettingsFrame(settings, false);
FutureCallback callback = new FutureCallback();
clientSession.settings(frame, callback);
callback.get(5, TimeUnit.SECONDS);
await().atMost(5, TimeUnit.SECONDS).until(() -> clientStream1.getRecvWindow() == 0);
assertEquals(FlowControlStrategy.DEFAULT_WINDOW_SIZE, clientStream1.getSendWindow());
assertEquals(0, clientStream1.getRecvWindow());
settingsLatch.await(5, TimeUnit.SECONDS);
// Now create a new stream, it must pick up the new value.
MetaData.Request request2 = newRequest("POST", HttpFields.EMPTY);
@ -344,6 +344,11 @@ public abstract class FlowControlStrategyTest
.thenRun(settingsLatch::countDown);
assertTrue(settingsLatch.await(5, TimeUnit.SECONDS));
await().atMost(5, TimeUnit.SECONDS).until(() ->
{
AbstractFlowControlStrategy flow = (AbstractFlowControlStrategy)((HTTP2Session)session).getFlowControlStrategy();
return flow.getInitialStreamRecvWindow() == windowSize;
});
CountDownLatch dataLatch = new CountDownLatch(1);
Exchanger<Stream.Data> exchanger = new Exchanger<>();
@ -408,13 +413,14 @@ public abstract class FlowControlStrategyTest
{
int windowSize = 1536;
Exchanger<Stream.Data> exchanger = new Exchanger<>();
CountDownLatch settingsLatch = new CountDownLatch(1);
CountDownLatch dataLatch = new CountDownLatch(1);
AtomicReference<HTTP2Session> serverSessionRef = new AtomicReference<>();
start(new ServerSessionListener()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
serverSessionRef.set((HTTP2Session)session);
Map<Integer, Integer> settings = new HashMap<>();
settings.put(SettingsFrame.INITIAL_WINDOW_SIZE, windowSize);
return settings;
@ -468,21 +474,18 @@ public abstract class FlowControlStrategyTest
}
});
Session session = newClient(new Session.Listener()
{
@Override
public void onSettings(Session session, SettingsFrame frame)
{
settingsLatch.countDown();
}
});
Session clientSession = newClient(new Session.Listener() {});
assertTrue(settingsLatch.await(5, TimeUnit.SECONDS));
await().atMost(5, TimeUnit.SECONDS).until(() ->
{
AbstractFlowControlStrategy flow = (AbstractFlowControlStrategy)serverSessionRef.get().getFlowControlStrategy();
return flow.getInitialStreamRecvWindow() == windowSize;
});
MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
HeadersFrame requestFrame = new HeadersFrame(metaData, null, false);
FuturePromise<Stream> streamPromise = new FuturePromise<>();
session.newStream(requestFrame, streamPromise, null);
clientSession.newStream(requestFrame, streamPromise, null);
Stream stream = streamPromise.get(5, TimeUnit.SECONDS);
int length = 5 * windowSize;

View File

@ -39,6 +39,7 @@ import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
@ -1252,4 +1253,73 @@ public class GoAwayTest extends AbstractTest
long afterPort = response.getHeaders().getLongField("X-Remote-Port");
assertNotEquals(primePort, afterPort);
}
@Test
public void testGoAwayNonZeroStreamId() throws Exception
{
CountDownLatch serverGoAwayLatch = new CountDownLatch(1);
CountDownLatch serverFailureLatch = new CountDownLatch(1);
CountDownLatch serverCloseLatch = new CountDownLatch(1);
start(new ServerSessionListener()
{
@Override
public void onGoAway(Session session, GoAwayFrame frame)
{
serverGoAwayLatch.countDown();
}
@Override
public void onFailure(Session session, Throwable failure, Callback callback)
{
serverFailureLatch.countDown();
callback.succeeded();
}
@Override
public void onClose(Session session, GoAwayFrame frame, Callback callback)
{
serverCloseLatch.countDown();
callback.succeeded();
}
});
CountDownLatch clientGoAwayLatch = new CountDownLatch(1);
CountDownLatch clientCloseLatch = new CountDownLatch(1);
Session clientSession = newClientSession(new Session.Listener()
{
@Override
public void onGoAway(Session session, GoAwayFrame frame)
{
clientGoAwayLatch.countDown();
}
@Override
public void onClose(Session session, GoAwayFrame frame, Callback callback)
{
clientCloseLatch.countDown();
callback.succeeded();
}
});
// Wait until the client has finished the previous writes.
Thread.sleep(1000);
// Write an invalid GOAWAY frame.
ByteBuffer byteBuffer = ByteBuffer.allocate(17)
.put((byte)0)
.put((byte)0)
.put((byte)8)
.put((byte)FrameType.GO_AWAY.getType())
.put((byte)0)
.putInt(1) // Non-Zero Stream ID
.putInt(0)
.putInt(ErrorCode.PROTOCOL_ERROR.code)
.flip();
((HTTP2Session)clientSession).getEndPoint().write(Callback.NOOP, byteBuffer);
assertFalse(serverGoAwayLatch.await(1, TimeUnit.SECONDS));
assertTrue(serverFailureLatch.await(5, TimeUnit.SECONDS));
assertTrue(serverCloseLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientGoAwayLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientCloseLatch.await(5, TimeUnit.SECONDS));
}
}

View File

@ -25,7 +25,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpFields;
@ -153,7 +152,8 @@ public class HTTP2CServerTest extends AbstractServerTest
final AtomicReference<HeadersFrame> headersRef = new AtomicReference<>();
final AtomicReference<DataFrame> dataRef = new AtomicReference<>();
final AtomicReference<CountDownLatch> latchRef = new AtomicReference<>(new CountDownLatch(2));
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
@ -168,8 +168,7 @@ public class HTTP2CServerTest extends AbstractServerTest
dataRef.set(frame);
latchRef.get().countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -249,7 +248,8 @@ public class HTTP2CServerTest extends AbstractServerTest
final AtomicReference<HeadersFrame> headersRef = new AtomicReference<>();
final AtomicReference<DataFrame> dataRef = new AtomicReference<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onSettings(SettingsFrame frame)
@ -270,8 +270,7 @@ public class HTTP2CServerTest extends AbstractServerTest
dataRef.set(frame);
latch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);

View File

@ -27,7 +27,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpVersion;
@ -97,15 +96,15 @@ public class HTTP2ServerTest extends AbstractServerTest
}
CountDownLatch latch = new CountDownLatch(1);
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onGoAway(GoAwayFrame frame)
{
latch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -143,7 +142,8 @@ public class HTTP2ServerTest extends AbstractServerTest
}
AtomicReference<HeadersFrame> frameRef = new AtomicReference<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onSettings(SettingsFrame frame)
@ -157,8 +157,7 @@ public class HTTP2ServerTest extends AbstractServerTest
frameRef.set(frame);
latch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -203,7 +202,8 @@ public class HTTP2ServerTest extends AbstractServerTest
AtomicReference<HeadersFrame> headersRef = new AtomicReference<>();
AtomicReference<DataFrame> dataRef = new AtomicReference<>();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onSettings(SettingsFrame frame)
@ -224,8 +224,7 @@ public class HTTP2ServerTest extends AbstractServerTest
dataRef.set(frame);
latch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -271,7 +270,8 @@ public class HTTP2ServerTest extends AbstractServerTest
output.write(BufferUtil.toArray(buffer));
}
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onGoAway(GoAwayFrame frame)
@ -279,8 +279,7 @@ public class HTTP2ServerTest extends AbstractServerTest
assertEquals(ErrorCode.FRAME_SIZE_ERROR.code, frame.getError());
latch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -317,7 +316,8 @@ public class HTTP2ServerTest extends AbstractServerTest
output.write(BufferUtil.toArray(buffer));
}
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onGoAway(GoAwayFrame frame)
@ -325,8 +325,7 @@ public class HTTP2ServerTest extends AbstractServerTest
assertEquals(ErrorCode.PROTOCOL_ERROR.code, frame.getError());
latch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
parseResponse(client, parser);
@ -389,8 +388,8 @@ public class HTTP2ServerTest extends AbstractServerTest
// The server will close the connection abruptly since it
// cannot write and therefore cannot even send the GO_AWAY.
Parser parser = new Parser(bufferPool, new Parser.Listener() {}, 4096, 8192);
parser.init(UnaryOperator.identity());
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener() {});
boolean closed = parseResponse(client, parser, 2 * delay);
assertTrue(closed);
}
@ -429,8 +428,8 @@ public class HTTP2ServerTest extends AbstractServerTest
}
output.flush();
Parser parser = new Parser(bufferPool, new Parser.Listener() {}, 4096, 8192);
parser.init(UnaryOperator.identity());
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener() {});
boolean closed = parseResponse(client, parser);
assertTrue(closed);
@ -586,7 +585,7 @@ public class HTTP2ServerTest extends AbstractServerTest
return null;
}
});
generator = new Generator(bufferPool, 4096, 4);
generator = new Generator(bufferPool, 4);
ByteBufferPool.Accumulator accumulator = frames.call();
@ -602,7 +601,8 @@ public class HTTP2ServerTest extends AbstractServerTest
assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
CountDownLatch clientLatch = new CountDownLatch(1);
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onHeaders(HeadersFrame frame)
@ -610,8 +610,7 @@ public class HTTP2ServerTest extends AbstractServerTest
if (frame.isEndStream())
clientLatch.countDown();
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
boolean closed = parseResponse(client, parser);
assertTrue(clientLatch.await(5, TimeUnit.SECONDS));

View File

@ -31,7 +31,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;
import org.eclipse.jetty.client.Connection;
@ -550,7 +549,8 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
OutputStream output = socket.getOutputStream();
InputStream input = socket.getInputStream();
ServerParser parser = new ServerParser(bufferPool, new ServerParser.Listener()
ServerParser parser = new ServerParser(bufferPool, 8192, RateControl.NO_RATE_CONTROL);
parser.init(new ServerParser.Listener()
{
@Override
public void onPreface()
@ -602,8 +602,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
x.printStackTrace();
}
}
}, 4096, 8192, RateControl.NO_RATE_CONTROL);
parser.init(UnaryOperator.identity());
});
byte[] bytes = new byte[1024];
while (true)
@ -674,7 +673,8 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
// Disable checks for invalid headers.
((HTTP2Session)stream.getSession()).getGenerator().setValidateHpackEncoding(false);
Generator generator = ((HTTP2Session)stream.getSession()).getGenerator();
generator.getHpackEncoder().setValidateEncoding(false);
// Produce an invalid HPACK block by adding a request pseudo-header to the response.
HttpFields fields = HttpFields.build()
.put(":method", "get");

View File

@ -68,7 +68,7 @@ public class InterleavingTest extends AbstractTest
}
});
int maxFrameSize = Frame.DEFAULT_MAX_LENGTH + 1;
int maxFrameSize = Frame.DEFAULT_MAX_SIZE + 1;
Session session = newClientSession(new Session.Listener()
{
@Override

View File

@ -31,7 +31,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
@ -147,6 +146,7 @@ public class PrefaceTest extends AbstractTest
session.close(ErrorCode.NO_ERROR.code, null, Callback.NOOP);
}
});
connector.setIdleTimeout(1000);
ByteBufferPool bufferPool = http2Client.getByteBufferPool();
try (SocketChannel socket = SocketChannel.open())
@ -167,7 +167,8 @@ public class PrefaceTest extends AbstractTest
Queue<SettingsFrame> settings = new ArrayDeque<>();
AtomicBoolean closed = new AtomicBoolean();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onSettings(SettingsFrame frame)
@ -180,8 +181,7 @@ public class PrefaceTest extends AbstractTest
{
closed.set(true);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true)
@ -311,7 +311,8 @@ public class PrefaceTest extends AbstractTest
CountDownLatch clientSettingsLatch = new CountDownLatch(1);
AtomicBoolean responded = new AtomicBoolean();
Parser parser = new Parser(bufferPool, new Parser.Listener()
Parser parser = new Parser(bufferPool, 8192);
parser.init(new Parser.Listener()
{
@Override
public void onSettings(SettingsFrame frame)
@ -328,8 +329,7 @@ public class PrefaceTest extends AbstractTest
if (frame.isEndStream())
responded.set(true);
}
}, 4096, 8192);
parser.init(UnaryOperator.identity());
});
// HTTP/2 parsing.
while (true)

View File

@ -0,0 +1,366 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http2.tests;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.internal.Flags;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class SettingsTest extends AbstractTest
{
@Test
public void testSettingsNonZeroStreamId() throws Exception
{
AtomicReference<CountDownLatch> serverSettingsLatch = new AtomicReference<>(null);
CountDownLatch serverFailureLatch = new CountDownLatch(1);
CountDownLatch serverCloseLatch = new CountDownLatch(1);
start(new ServerSessionListener()
{
@Override
public void onSettings(Session session, SettingsFrame frame)
{
CountDownLatch latch = serverSettingsLatch.get();
if (latch != null)
latch.countDown();
}
@Override
public void onFailure(Session session, Throwable failure, Callback callback)
{
serverFailureLatch.countDown();
callback.succeeded();
}
@Override
public void onClose(Session session, GoAwayFrame frame, Callback callback)
{
serverCloseLatch.countDown();
callback.succeeded();
}
});
CountDownLatch clientGoAwayLatch = new CountDownLatch(1);
CountDownLatch clientCloseLatch = new CountDownLatch(1);
Session clientSession = newClientSession(new Session.Listener()
{
@Override
public void onGoAway(Session session, GoAwayFrame frame)
{
clientGoAwayLatch.countDown();
}
@Override
public void onClose(Session session, GoAwayFrame frame, Callback callback)
{
clientCloseLatch.countDown();
callback.succeeded();
}
});
// Wait until the client has finished the previous writes.
Thread.sleep(1000);
// Set the SETTINGS latch now, to avoid that it
// is counted down during connection establishment.
serverSettingsLatch.set(new CountDownLatch(1));
// Write an invalid SETTINGS frame.
ByteBuffer byteBuffer = ByteBuffer.allocate(17)
.put((byte)0)
.put((byte)0)
.put((byte)0)
.put((byte)FrameType.SETTINGS.getType())
.put((byte)0)
.putInt(1) // Non-Zero Stream ID
.flip();
((HTTP2Session)clientSession).getEndPoint().write(Callback.NOOP, byteBuffer);
Assertions.assertFalse(serverSettingsLatch.get().await(1, TimeUnit.SECONDS));
Assertions.assertTrue(serverFailureLatch.await(5, TimeUnit.SECONDS));
Assertions.assertTrue(serverCloseLatch.await(5, TimeUnit.SECONDS));
Assertions.assertTrue(clientGoAwayLatch.await(5, TimeUnit.SECONDS));
Assertions.assertTrue(clientCloseLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testSettingsReplyWithPayload() throws Exception
{
AtomicReference<CountDownLatch> serverSettingsLatch = new AtomicReference<>(null);
CountDownLatch serverFailureLatch = new CountDownLatch(1);
CountDownLatch serverCloseLatch = new CountDownLatch(1);
start(new ServerSessionListener()
{
@Override
public void onSettings(Session session, SettingsFrame frame)
{
CountDownLatch latch = serverSettingsLatch.get();
if (latch != null)
latch.countDown();
}
@Override
public void onFailure(Session session, Throwable failure, Callback callback)
{
serverFailureLatch.countDown();
callback.succeeded();
}
@Override
public void onClose(Session session, GoAwayFrame frame, Callback callback)
{
serverCloseLatch.countDown();
callback.succeeded();
}
});
CountDownLatch clientGoAwayLatch = new CountDownLatch(1);
CountDownLatch clientCloseLatch = new CountDownLatch(1);
Session clientSession = newClientSession(new Session.Listener()
{
@Override
public void onGoAway(Session session, GoAwayFrame frame)
{
clientGoAwayLatch.countDown();
}
@Override
public void onClose(Session session, GoAwayFrame frame, Callback callback)
{
clientCloseLatch.countDown();
callback.succeeded();
}
});
// Wait until the client has finished the previous writes.
Thread.sleep(1000);
// Set the SETTINGS latch now, to avoid that it
// is counted down during connection establishment.
serverSettingsLatch.set(new CountDownLatch(1));
// Write an invalid SETTINGS frame.
ByteBuffer byteBuffer = ByteBuffer.allocate(17)
.put((byte)0)
.put((byte)0)
.put((byte)6)
.put((byte)FrameType.SETTINGS.getType())
.put((byte)Flags.ACK)
.putInt(0)
.putShort((short)SettingsFrame.ENABLE_PUSH)
.putInt(1)
.flip();
((HTTP2Session)clientSession).getEndPoint().write(Callback.NOOP, byteBuffer);
Assertions.assertFalse(serverSettingsLatch.get().await(1, TimeUnit.SECONDS));
Assertions.assertTrue(serverFailureLatch.await(5, TimeUnit.SECONDS));
Assertions.assertTrue(serverCloseLatch.await(5, TimeUnit.SECONDS));
Assertions.assertTrue(clientGoAwayLatch.await(5, TimeUnit.SECONDS));
Assertions.assertTrue(clientCloseLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testInvalidEnablePush() throws Exception
{
CountDownLatch serverFailureLatch = new CountDownLatch(1);
start(new ServerSessionListener()
{
@Override
public void onFailure(Session session, Throwable failure, Callback callback)
{
serverFailureLatch.countDown();
callback.succeeded();
}
});
newClientSession(new Session.Listener()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
Map<Integer, Integer> settings = new HashMap<>();
settings.put(SettingsFrame.ENABLE_PUSH, 2); // Invalid value.
return settings;
}
});
Assertions.assertTrue(serverFailureLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testServerSendsEnablePush() throws Exception
{
start(new ServerSessionListener()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
Map<Integer, Integer> settings = new HashMap<>();
// Servers cannot send "enable_push==1".
settings.put(SettingsFrame.ENABLE_PUSH, 1);
return settings;
}
});
CountDownLatch clientFailureLatch = new CountDownLatch(1);
newClientSession(new Session.Listener()
{
@Override
public void onFailure(Session session, Throwable failure, Callback callback)
{
clientFailureLatch.countDown();
callback.succeeded();
}
});
Assertions.assertTrue(clientFailureLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testServerCannotSendsPushPromiseWithPushDisabled() throws Exception
{
CountDownLatch serverPushFailureLatch = new CountDownLatch(1);
start(new ServerSessionListener()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpStatus.OK_200, null, HttpVersion.HTTP_2, HttpFields.EMPTY);
stream.headers(new HeadersFrame(stream.getId(), response, null, true))
.thenAccept(s ->
{
MetaData.Request push = newRequest("GET", "/push", HttpFields.EMPTY);
try
{
s.push(new PushPromiseFrame(s.getId(), push), new Stream.Listener() {});
}
catch (IllegalStateException x)
{
serverPushFailureLatch.countDown();
}
});
return null;
}
});
Session clientSession = newClientSession(new Session.Listener()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
Map<Integer, Integer> settings = new HashMap<>();
// Disable push.
settings.put(SettingsFrame.ENABLE_PUSH, 0);
return settings;
}
});
CountDownLatch clientResponseLatch = new CountDownLatch(1);
CountDownLatch clientPushLatch = new CountDownLatch(1);
MetaData.Request request = newRequest("GET", HttpFields.EMPTY);
HeadersFrame frame = new HeadersFrame(request, null, true);
clientSession.newStream(frame, new Stream.Listener()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
clientResponseLatch.countDown();
}
@Override
public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
{
clientPushLatch.countDown();
return null;
}
});
Assertions.assertTrue(serverPushFailureLatch.await(5, TimeUnit.SECONDS));
Assertions.assertTrue(clientResponseLatch.await(5, TimeUnit.SECONDS));
Assertions.assertFalse(clientPushLatch.await(1, TimeUnit.SECONDS));
}
@Test
public void testClientReceivesPushPromiseWhenPushDisabled() throws Exception
{
start(new ServerSessionListener()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
try
{
HTTP2Session session = (HTTP2Session)stream.getSession();
ByteBufferPool.Accumulator accumulator = new ByteBufferPool.Accumulator();
MetaData.Request push = newRequest("GET", "/push", HttpFields.EMPTY);
PushPromiseFrame pushFrame = new PushPromiseFrame(stream.getId(), 2, push);
session.getGenerator().control(accumulator, pushFrame);
session.getEndPoint().write(Callback.NOOP, accumulator.getByteBuffers().toArray(ByteBuffer[]::new));
return null;
}
catch (HpackException x)
{
return null;
}
}
});
CountDownLatch clientFailureLatch = new CountDownLatch(1);
Session clientSession = newClientSession(new Session.Listener()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
Map<Integer, Integer> settings = new HashMap<>();
// Disable push.
settings.put(SettingsFrame.ENABLE_PUSH, 0);
return settings;
}
@Override
public void onFailure(Session session, Throwable failure, Callback callback)
{
clientFailureLatch.countDown();
callback.succeeded();
}
});
// Wait until the server has finished the previous writes.
Thread.sleep(1000);
MetaData.Request request = newRequest("GET", HttpFields.EMPTY);
HeadersFrame frame = new HeadersFrame(request, null, true);
clientSession.newStream(frame, new Stream.Listener() {});
Assertions.assertTrue(clientFailureLatch.await(5, TimeUnit.SECONDS));
}
}

View File

@ -36,6 +36,7 @@ import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
@ -399,7 +400,8 @@ public class TrailersTest extends AbstractTest
.thenAccept(s ->
{
// Disable checks for invalid headers.
((HTTP2Session)session).getGenerator().setValidateHpackEncoding(false);
Generator generator = ((HTTP2Session)session).getGenerator();
generator.getHpackEncoder().setValidateEncoding(false);
// Invalid trailer: cannot contain pseudo headers.
HttpFields.Mutable trailerFields = HttpFields.build();
trailerFields.put(HttpHeader.C_METHOD, "GET");

View File

@ -173,7 +173,7 @@ public class ClientHTTP3Session extends ClientProtocolSession
{
int maxTableCapacity = value.intValue();
encoder.setMaxTableCapacity(maxTableCapacity);
encoder.setTableCapacity(Math.min(maxTableCapacity, configuration.getInitialEncoderTableCapacity()));
encoder.setTableCapacity(Math.min(maxTableCapacity, configuration.getMaxEncoderTableCapacity()));
}
else if (key == SettingsFrame.MAX_FIELD_SECTION_SIZE)
{

View File

@ -28,8 +28,8 @@ public class HTTP3Configuration
private boolean useInputDirectByteBuffers = true;
private boolean useOutputDirectByteBuffers = true;
private int maxBlockedStreams = 64;
private int maxTableCapacity = 64 * 1024;
private int initialTableCapacity = 64 * 1024;
private int maxDecoderTableCapacity = 64 * 1024;
private int maxEncoderTableCapacity = 64 * 1024;
private int maxRequestHeadersSize = 8 * 1024;
private int maxResponseHeadersSize = 8 * 1024;
@ -122,7 +122,7 @@ public class HTTP3Configuration
@ManagedAttribute("The local QPACK max decoder dynamic table capacity")
public int getMaxDecoderTableCapacity()
{
return maxTableCapacity;
return maxDecoderTableCapacity;
}
/**
@ -132,17 +132,17 @@ public class HTTP3Configuration
* communicated to the remote QPACK encoder via the SETTINGS frame.</p>
*
* @param maxTableCapacity the QPACK decoder dynamic table max capacity
* @see #setInitialEncoderTableCapacity(int)
* @see #setMaxEncoderTableCapacity(int)
*/
public void setMaxDecoderTableCapacity(int maxTableCapacity)
{
this.maxTableCapacity = maxTableCapacity;
this.maxDecoderTableCapacity = maxTableCapacity;
}
@ManagedAttribute("The local QPACK initial encoder dynamic table capacity")
public int getInitialEncoderTableCapacity()
public int getMaxEncoderTableCapacity()
{
return initialTableCapacity;
return maxEncoderTableCapacity;
}
/**
@ -151,12 +151,12 @@ public class HTTP3Configuration
* <p>This value is configured in the local QPACK encoder, and may be
* overwritten by a smaller value received via the SETTINGS frame.</p>
*
* @param initialTableCapacity the QPACK encoder dynamic table initial capacity
* @param maxTableCapacity the QPACK encoder dynamic table initial capacity
* @see #setMaxDecoderTableCapacity(int)
*/
public void setInitialEncoderTableCapacity(int initialTableCapacity)
public void setMaxEncoderTableCapacity(int maxTableCapacity)
{
this.initialTableCapacity = initialTableCapacity;
this.maxEncoderTableCapacity = maxTableCapacity;
}
@ManagedAttribute("The max number of QPACK blocked streams")

View File

@ -12,6 +12,7 @@ experimental
http2
jna
quiche
work
[lib]
lib/http3/*.jar

View File

@ -172,7 +172,7 @@ public class ServerHTTP3Session extends ServerProtocolSession
{
int maxTableCapacity = value.intValue();
encoder.setMaxTableCapacity(maxTableCapacity);
encoder.setTableCapacity(Math.min(maxTableCapacity, configuration.getInitialEncoderTableCapacity()));
encoder.setTableCapacity(Math.min(maxTableCapacity, configuration.getMaxEncoderTableCapacity()));
}
else if (key == SettingsFrame.MAX_FIELD_SECTION_SIZE)
{

View File

@ -13,8 +13,10 @@
package org.eclipse.jetty.http3.tests;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.util.concurrent.TimeUnit;
import javax.management.MBeanServer;
@ -35,15 +37,21 @@ import org.eclipse.jetty.jmx.MBeanContainer;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
@ExtendWith(WorkDirExtension.class)
public class AbstractClientServerTest
{
public WorkDir workDir;
@RegisterExtension
final BeforeTestExecutionCallback printMethodName = context ->
System.err.printf("Running %s.%s() %s%n", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), context.getDisplayName());
@ -81,6 +89,7 @@ public class AbstractClientServerTest
serverThreads.setName("server");
server = new Server(serverThreads);
connector = new HTTP3ServerConnector(server, sslContextFactory, serverConnectionFactory);
connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir());
server.addConnector(connector);
MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
server.addBean(mbeanContainer);
@ -88,8 +97,16 @@ public class AbstractClientServerTest
protected void startClient() throws Exception
{
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (InputStream is = getClass().getResourceAsStream("/keystore.p12"))
{
trustStore.load(is, "storepwd".toCharArray());
}
http3Client = new HTTP3Client();
http3Client.getQuicConfiguration().setVerifyPeerCertificates(false);
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client();
clientSslContextFactory.setTrustStore(trustStore);
http3Client.getClientConnector().setSslContextFactory(clientSslContextFactory);
httpClient = new HttpClient(new HttpClientTransportDynamic(new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client)));
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");

View File

@ -382,7 +382,7 @@ public class ClientServerTest extends AbstractClientServerTest
http3Configuration.setMaxRequestHeadersSize(maxRequestHeadersSize);
// Disable the dynamic table, otherwise the large header
// is sent as string literal on the encoder stream.
http3Configuration.setInitialEncoderTableCapacity(0);
http3Configuration.setMaxEncoderTableCapacity(0);
Session.Client clientSession = newSession(new Session.Client.Listener() {});
CountDownLatch requestFailureLatch = new CountDownLatch(1);
@ -463,7 +463,7 @@ public class ClientServerTest extends AbstractClientServerTest
HTTP3Configuration http3Configuration = h3.getHTTP3Configuration();
// Disable the dynamic table, otherwise the large header
// is sent as string literal on the encoder stream.
http3Configuration.setInitialEncoderTableCapacity(0);
http3Configuration.setMaxEncoderTableCapacity(0);
http3Configuration.setMaxResponseHeadersSize(maxResponseHeadersSize);
Session.Client clientSession = newSession(new Session.Client.Listener()

View File

@ -117,6 +117,7 @@ public class ClientConnector extends ContainerLifeCycle
{
this.configurator = Objects.requireNonNull(configurator);
addBean(configurator);
configurator.addBean(this, false);
}
/**
@ -588,7 +589,7 @@ public class ClientConnector extends ContainerLifeCycle
/**
* <p>Configures a {@link ClientConnector}.</p>
*/
public static class Configurator
public static class Configurator extends ContainerLifeCycle
{
/**
* <p>Returns whether the connection to a given {@link SocketAddress} is intrinsically secure.</p>

View File

@ -80,7 +80,11 @@ public class ClientQuicConnection extends QuicConnection
QuicheConfig quicheConfig = new QuicheConfig();
quicheConfig.setApplicationProtos(protocols.toArray(String[]::new));
quicheConfig.setDisableActiveMigration(quicConfiguration.isDisableActiveMigration());
quicheConfig.setVerifyPeer(quicConfiguration.isVerifyPeerCertificates());
quicheConfig.setVerifyPeer(!connector.getSslContextFactory().isTrustAll());
Map<String, Object> implCtx = quicConfiguration.getImplementationConfiguration();
quicheConfig.setTrustedCertsPemPath((String)implCtx.get(QuicClientConnectorConfigurator.TRUSTED_CERTIFICATES_PEM_PATH_KEY));
quicheConfig.setPrivKeyPemPath((String)implCtx.get(QuicClientConnectorConfigurator.PRIVATE_KEY_PEM_PATH_KEY));
quicheConfig.setCertChainPemPath((String)implCtx.get(QuicClientConnectorConfigurator.CERTIFICATE_CHAIN_PEM_PATH_KEY));
// Idle timeouts must not be managed by Quiche.
quicheConfig.setMaxIdleTimeout(0L);
quicheConfig.setInitialMaxData((long)quicConfiguration.getSessionRecvWindow());
@ -147,6 +151,13 @@ public class ClientQuicConnection extends QuicConnection
return null;
}
@Override
protected void onFailure(Throwable failure)
{
pendingSessions.values().forEach(session -> outwardClose(session, failure));
super.onFailure(failure);
}
@Override
public boolean onIdleExpired()
{

View File

@ -19,6 +19,9 @@ import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Map;
import java.util.Objects;
import java.util.function.UnaryOperator;
@ -30,6 +33,10 @@ import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.SocketChannelEndPoint;
import org.eclipse.jetty.quic.common.QuicConfiguration;
import org.eclipse.jetty.quic.quiche.PemExporter;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>A QUIC specific {@link ClientConnector.Configurator}.</p>
@ -41,8 +48,17 @@ import org.eclipse.jetty.quic.common.QuicConfiguration;
*/
public class QuicClientConnectorConfigurator extends ClientConnector.Configurator
{
private static final Logger LOG = LoggerFactory.getLogger(QuicClientConnectorConfigurator.class);
static final String PRIVATE_KEY_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".privateKeyPemPath";
static final String CERTIFICATE_CHAIN_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".certificateChainPemPath";
static final String TRUSTED_CERTIFICATES_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".trustedCertificatesPemPath";
private final QuicConfiguration configuration = new QuicConfiguration();
private final UnaryOperator<Connection> configurator;
private Path privateKeyPemPath;
private Path certificateChainPemPath;
private Path trustedCertificatesPemPath;
public QuicClientConnectorConfigurator()
{
@ -56,7 +72,6 @@ public class QuicClientConnectorConfigurator extends ClientConnector.Configurato
configuration.setSessionRecvWindow(16 * 1024 * 1024);
configuration.setBidirectionalStreamRecvWindow(8 * 1024 * 1024);
configuration.setDisableActiveMigration(true);
configuration.setVerifyPeerCertificates(true);
}
public QuicConfiguration getQuicConfiguration()
@ -64,6 +79,64 @@ public class QuicClientConnectorConfigurator extends ClientConnector.Configurato
return configuration;
}
@Override
protected void doStart() throws Exception
{
Path pemWorkDirectory = configuration.getPemWorkDirectory();
ClientConnector clientConnector = getBean(ClientConnector.class);
SslContextFactory.Client sslContextFactory = clientConnector.getSslContextFactory();
KeyStore trustStore = sslContextFactory.getTrustStore();
if (trustStore != null)
{
trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, pemWorkDirectory != null ? pemWorkDirectory : Path.of(System.getProperty("java.io.tmpdir")));
configuration.getImplementationConfiguration().put(TRUSTED_CERTIFICATES_PEM_PATH_KEY, trustedCertificatesPemPath.toString());
}
String certAlias = sslContextFactory.getCertAlias();
if (certAlias != null)
{
if (pemWorkDirectory == null)
throw new IllegalStateException("No PEM work directory configured");
KeyStore keyStore = sslContextFactory.getKeyStore();
String keyManagerPassword = sslContextFactory.getKeyManagerPassword();
char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray();
Path[] keyPair = PemExporter.exportKeyPair(keyStore, certAlias, password, pemWorkDirectory);
privateKeyPemPath = keyPair[0];
certificateChainPemPath = keyPair[1];
configuration.getImplementationConfiguration().put(PRIVATE_KEY_PEM_PATH_KEY, privateKeyPemPath.toString());
configuration.getImplementationConfiguration().put(CERTIFICATE_CHAIN_PEM_PATH_KEY, certificateChainPemPath.toString());
}
super.doStart();
}
@Override
protected void doStop() throws Exception
{
super.doStop();
deleteFile(privateKeyPemPath);
privateKeyPemPath = null;
configuration.getImplementationConfiguration().remove(PRIVATE_KEY_PEM_PATH_KEY);
deleteFile(certificateChainPemPath);
certificateChainPemPath = null;
configuration.getImplementationConfiguration().remove(CERTIFICATE_CHAIN_PEM_PATH_KEY);
deleteFile(trustedCertificatesPemPath);
trustedCertificatesPemPath = null;
configuration.getImplementationConfiguration().remove(TRUSTED_CERTIFICATES_PEM_PATH_KEY);
}
private void deleteFile(Path file)
{
try
{
if (file != null)
Files.delete(file);
}
catch (IOException x)
{
if (LOG.isDebugEnabled())
LOG.debug("could not delete {}", file, x);
}
}
@Override
public boolean isIntrinsicallySecure(ClientConnector clientConnector, SocketAddress address)
{
@ -74,6 +147,7 @@ public class QuicClientConnectorConfigurator extends ClientConnector.Configurato
public ChannelWithAddress newChannelWithAddress(ClientConnector clientConnector, SocketAddress address, Map<String, Object> context) throws IOException
{
context.put(QuicConfiguration.CONTEXT_KEY, configuration);
DatagramChannel channel = DatagramChannel.open();
if (clientConnector.getBindAddress() == null)
{

View File

@ -13,6 +13,8 @@
package org.eclipse.jetty.quic.client;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@ -34,6 +36,8 @@ import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
@ -41,12 +45,16 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
@ExtendWith(WorkDirExtension.class)
public class End2EndClientTest
{
public WorkDir workDir;
private Server server;
private QuicServerConnector connector;
private HttpClient client;
@ -61,8 +69,14 @@ public class End2EndClientTest
@BeforeEach
public void setUp() throws Exception
{
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = getClass().getResourceAsStream("/keystore.p12"))
{
keyStore.load(is, "storepwd".toCharArray());
}
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStore(keyStore);
sslContextFactory.setKeyStorePassword("storepwd");
server = new Server();
@ -71,6 +85,7 @@ public class End2EndClientTest
HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration);
HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfiguration);
connector = new QuicServerConnector(server, sslContextFactory, http1, http2);
connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir());
server.addConnector(connector);
server.setHandler(new Handler.Abstract()
@ -85,11 +100,13 @@ public class End2EndClientTest
server.start();
ClientConnector clientConnector = new ClientConnector(new QuicClientConnectorConfigurator());
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client();
clientSslContextFactory.setTrustStore(keyStore);
clientConnector.setSslContextFactory(clientSslContextFactory);
ClientConnectionFactory.Info http1Info = HttpClientConnectionFactory.HTTP11;
ClientConnectionFactoryOverHTTP2.HTTP2 http2Info = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client());
QuicClientConnectorConfigurator configurator = new QuicClientConnectorConfigurator();
configurator.getQuicConfiguration().setVerifyPeerCertificates(false);
HttpClientTransportDynamic transport = new HttpClientTransportDynamic(new ClientConnector(configurator), http1Info, http2Info);
ClientConnectionFactoryOverHTTP2.HTTP2 http2Info = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector));
HttpClientTransportDynamic transport = new HttpClientTransportDynamic(clientConnector, http1Info, http2Info);
client = new HttpClient(transport);
client.start();
}

View File

@ -0,0 +1,160 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.quic.client;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.transport.HttpClientConnectionFactory;
import org.eclipse.jetty.client.transport.HttpClientTransportDynamic;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.quic.server.QuicServerConnector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(WorkDirExtension.class)
public class End2EndClientWithClientCertAuthTest
{
public WorkDir workDir;
private Server server;
private QuicServerConnector connector;
private HttpClient client;
private final String responseContent = """
<html>
\t<body>
\t\tRequest served
\t</body>
</html>""";
private SslContextFactory.Server serverSslContextFactory;
@BeforeEach
public void setUp() throws Exception
{
Path workPath = workDir.getEmptyPathDir();
Path serverWorkPath = workPath.resolve("server");
Files.createDirectories(serverWorkPath);
Path clientWorkPath = workPath.resolve("client");
Files.createDirectories(clientWorkPath);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = getClass().getResourceAsStream("/keystore.p12"))
{
keyStore.load(is, "storepwd".toCharArray());
}
serverSslContextFactory = new SslContextFactory.Server();
serverSslContextFactory.setKeyStore(keyStore);
serverSslContextFactory.setKeyStorePassword("storepwd");
serverSslContextFactory.setTrustStore(keyStore);
serverSslContextFactory.setNeedClientAuth(true);
server = new Server();
HttpConfiguration httpConfiguration = new HttpConfiguration();
httpConfiguration.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration);
HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfiguration);
connector = new QuicServerConnector(server, serverSslContextFactory, http1, http2);
connector.getQuicConfiguration().setPemWorkDirectory(serverWorkPath);
server.addConnector(connector);
server.setHandler(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
Content.Sink.write(response, true, responseContent, callback);
return true;
}
});
server.start();
QuicClientConnectorConfigurator configurator = new QuicClientConnectorConfigurator();
configurator.getQuicConfiguration().setPemWorkDirectory(clientWorkPath);
ClientConnector clientConnector = new ClientConnector(configurator);
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client();
clientSslContextFactory.setCertAlias("mykey");
clientSslContextFactory.setKeyStore(keyStore);
clientSslContextFactory.setKeyStorePassword("storepwd");
clientSslContextFactory.setTrustStore(keyStore);
clientConnector.setSslContextFactory(clientSslContextFactory);
ClientConnectionFactory.Info http1Info = HttpClientConnectionFactory.HTTP11;
ClientConnectionFactoryOverHTTP2.HTTP2 http2Info = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector));
HttpClientTransportDynamic transport = new HttpClientTransportDynamic(clientConnector, http1Info, http2Info);
client = new HttpClient(transport);
client.start();
}
@AfterEach
public void tearDown()
{
LifeCycle.stop(client);
LifeCycle.stop(server);
}
@Test
public void testWorkingClientAuth() throws Exception
{
ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort())
.timeout(5, TimeUnit.SECONDS)
.send();
assertThat(response.getStatus(), is(200));
String contentAsString = response.getContentAsString();
assertThat(contentAsString, is(responseContent));
}
@Test
public void testServerRejectsClientInvalidCert() throws Exception
{
// remove the trust store config from the server
server.stop();
serverSslContextFactory.setTrustStore(null);
server.start();
assertThrows(TimeoutException.class, () -> client.newRequest("https://localhost:" + connector.getLocalPort())
.timeout(5, TimeUnit.SECONDS)
.send());
}
}

View File

@ -13,7 +13,10 @@
package org.eclipse.jetty.quic.common;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>A record that captures QUIC configuration parameters.</p>
@ -24,12 +27,13 @@ public class QuicConfiguration
private List<String> protocols = List.of();
private boolean disableActiveMigration;
private boolean verifyPeerCertificates;
private int maxBidirectionalRemoteStreams;
private int maxUnidirectionalRemoteStreams;
private int sessionRecvWindow;
private int bidirectionalStreamRecvWindow;
private int unidirectionalStreamRecvWindow;
private Path pemWorkDirectory;
private final Map<String, Object> implementationConfiguration = new HashMap<>();
public List<String> getProtocols()
{
@ -51,16 +55,6 @@ public class QuicConfiguration
this.disableActiveMigration = disableActiveMigration;
}
public boolean isVerifyPeerCertificates()
{
return verifyPeerCertificates;
}
public void setVerifyPeerCertificates(boolean verifyPeerCertificates)
{
this.verifyPeerCertificates = verifyPeerCertificates;
}
public int getMaxBidirectionalRemoteStreams()
{
return maxBidirectionalRemoteStreams;
@ -110,4 +104,19 @@ public class QuicConfiguration
{
this.unidirectionalStreamRecvWindow = unidirectionalStreamRecvWindow;
}
public Path getPemWorkDirectory()
{
return pemWorkDirectory;
}
public void setPemWorkDirectory(Path pemWorkDirectory)
{
this.pemWorkDirectory = pemWorkDirectory;
}
public Map<String, Object> getImplementationConfiguration()
{
return implementationConfiguration;
}
}

View File

@ -0,0 +1,130 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.quic.quiche;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.security.Key;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.Base64;
import java.util.Enumeration;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PemExporter
{
private static final Logger LOG = LoggerFactory.getLogger(PemExporter.class);
private static final byte[] BEGIN_KEY = "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] END_KEY = "-----END PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] BEGIN_CERT = "-----BEGIN CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] END_CERT = "-----END CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(StandardCharsets.US_ASCII);
private static final Base64.Encoder ENCODER = Base64.getMimeEncoder(64, LINE_SEPARATOR);
private PemExporter()
{
}
/**
* @return a temp file that gets deleted on exit
*/
public static Path exportTrustStore(KeyStore keyStore, Path targetFolder) throws Exception
{
if (!Files.isDirectory(targetFolder))
throw new IllegalArgumentException("Target folder is not a directory: " + targetFolder);
Path path = Files.createTempFile(targetFolder, "truststore-", ".crt");
try (OutputStream os = Files.newOutputStream(path))
{
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements())
{
String alias = aliases.nextElement();
Certificate cert = keyStore.getCertificate(alias);
writeAsPEM(os, cert);
}
}
return path;
}
/**
* @return [0] is the key file, [1] is the cert file.
*/
public static Path[] exportKeyPair(KeyStore keyStore, String alias, char[] keyPassword, Path targetFolder) throws Exception
{
if (!Files.isDirectory(targetFolder))
throw new IllegalArgumentException("Target folder is not a directory: " + targetFolder);
Path[] paths = new Path[2];
paths[1] = targetFolder.resolve(alias + ".crt");
try (OutputStream os = Files.newOutputStream(paths[1]))
{
Certificate[] certChain = keyStore.getCertificateChain(alias);
for (Certificate cert : certChain)
writeAsPEM(os, cert);
Files.setPosixFilePermissions(paths[1], Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE));
}
catch (UnsupportedOperationException e)
{
// Expected on Windows.
if (LOG.isDebugEnabled())
LOG.debug("Unable to set Posix file permissions", e);
}
paths[0] = targetFolder.resolve(alias + ".key");
try (OutputStream os = Files.newOutputStream(paths[0]))
{
Key key = keyStore.getKey(alias, keyPassword);
writeAsPEM(os, key);
Files.setPosixFilePermissions(paths[0], Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE));
}
catch (UnsupportedOperationException e)
{
// Expected on Windows.
if (LOG.isDebugEnabled())
LOG.debug("Unable to set Posix file permissions", e);
}
return paths;
}
private static void writeAsPEM(OutputStream outputStream, Key key) throws IOException
{
byte[] encoded = ENCODER.encode(key.getEncoded());
outputStream.write(BEGIN_KEY);
outputStream.write(LINE_SEPARATOR);
outputStream.write(encoded);
outputStream.write(LINE_SEPARATOR);
outputStream.write(END_KEY);
outputStream.write(LINE_SEPARATOR);
}
private static void writeAsPEM(OutputStream outputStream, Certificate certificate) throws CertificateEncodingException, IOException
{
byte[] encoded = ENCODER.encode(certificate.getEncoded());
outputStream.write(BEGIN_CERT);
outputStream.write(LINE_SEPARATOR);
outputStream.write(encoded);
outputStream.write(LINE_SEPARATOR);
outputStream.write(END_CERT);
outputStream.write(LINE_SEPARATOR);
}
}

View File

@ -119,4 +119,164 @@ public interface Quiche
return "?? " + err;
}
}
// QUIC Transport Error Codes: https://www.iana.org/assignments/quic/quic.xhtml#quic-transport-error-codes
interface quic_error
{
long NO_ERROR = 0,
INTERNAL_ERROR = 1,
CONNECTION_REFUSED = 2,
FLOW_CONTROL_ERROR = 3,
STREAM_LIMIT_ERROR = 4,
STREAM_STATE_ERROR = 5,
FINAL_SIZE_ERROR = 6,
FRAME_ENCODING_ERROR = 7,
TRANSPORT_PARAMETER_ERROR = 8,
CONNECTION_ID_LIMIT_ERROR = 9,
PROTOCOL_VIOLATION = 10,
INVALID_TOKEN = 11,
APPLICATION_ERROR = 12,
CRYPTO_BUFFER_EXCEEDED = 13,
KEY_UPDATE_ERROR = 14,
AEAD_LIMIT_REACHED = 15,
NO_VIABLE_PATH = 16,
VERSION_NEGOTIATION_ERROR = 17;
static String errToString(long err)
{
if (err == NO_ERROR)
return "NO_ERROR";
if (err == INTERNAL_ERROR)
return "INTERNAL_ERROR";
if (err == CONNECTION_REFUSED)
return "CONNECTION_REFUSED";
if (err == FLOW_CONTROL_ERROR)
return "FLOW_CONTROL_ERROR";
if (err == STREAM_LIMIT_ERROR)
return "STREAM_LIMIT_ERROR";
if (err == STREAM_STATE_ERROR)
return "STREAM_STATE_ERROR";
if (err == FINAL_SIZE_ERROR)
return "FINAL_SIZE_ERROR";
if (err == FRAME_ENCODING_ERROR)
return "FRAME_ENCODING_ERROR";
if (err == TRANSPORT_PARAMETER_ERROR)
return "TRANSPORT_PARAMETER_ERROR";
if (err == CONNECTION_ID_LIMIT_ERROR)
return "CONNECTION_ID_LIMIT_ERROR";
if (err == PROTOCOL_VIOLATION)
return "PROTOCOL_VIOLATION";
if (err == INVALID_TOKEN)
return "INVALID_TOKEN";
if (err == APPLICATION_ERROR)
return "APPLICATION_ERROR";
if (err == CRYPTO_BUFFER_EXCEEDED)
return "CRYPTO_BUFFER_EXCEEDED";
if (err == KEY_UPDATE_ERROR)
return "KEY_UPDATE_ERROR";
if (err == AEAD_LIMIT_REACHED)
return "AEAD_LIMIT_REACHED";
if (err == NO_VIABLE_PATH)
return "NO_VIABLE_PATH";
if (err == VERSION_NEGOTIATION_ERROR)
return "VERSION_NEGOTIATION_ERROR";
if (err >= 0x100 && err <= 0x01FF)
return "CRYPTO_ERROR " + tls_alert.errToString(err - 0x100);
return "?? " + err;
}
}
// TLS Alerts: https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-6
interface tls_alert
{
long CLOSE_NOTIFY = 0,
UNEXPECTED_MESSAGE = 10,
BAD_RECORD_MAC = 20,
RECORD_OVERFLOW = 22,
HANDSHAKE_FAILURE = 40,
BAD_CERTIFICATE = 42,
UNSUPPORTED_CERTIFICATE = 43,
CERTIFICATE_REVOKED = 44,
CERTIFICATE_EXPIRED = 45,
CERTIFICATE_UNKNOWN = 46,
ILLEGAL_PARAMETER = 47,
UNKNOWN_CA = 48,
ACCESS_DENIED = 49,
DECODE_ERROR = 50,
DECRYPT_ERROR = 51,
TOO_MANY_CIDS_REQUESTED = 52,
PROTOCOL_VERSION = 70,
INSUFFICIENT_SECURITY = 71,
INTERNAL_ERROR = 80,
INAPPROPRIATE_FALLBACK = 86,
USER_CANCELED = 90,
MISSING_EXTENSION = 109,
UNSUPPORTED_EXTENSION = 110,
UNRECOGNIZED_NAME = 112,
BAD_CERTIFICATE_STATUS_RESPONSE = 113,
UNKNOWN_PSK_IDENTITY = 115,
CERTIFICATE_REQUIRED = 116,
NO_APPLICATION_PROTOCOL = 120;
static String errToString(long err)
{
if (err == CLOSE_NOTIFY)
return "CLOSE_NOTIFY";
if (err == UNEXPECTED_MESSAGE)
return "UNEXPECTED_MESSAGE";
if (err == BAD_RECORD_MAC)
return "BAD_RECORD_MAC";
if (err == RECORD_OVERFLOW)
return "RECORD_OVERFLOW";
if (err == HANDSHAKE_FAILURE)
return "HANDSHAKE_FAILURE";
if (err == BAD_CERTIFICATE)
return "BAD_CERTIFICATE";
if (err == UNSUPPORTED_CERTIFICATE)
return "UNSUPPORTED_CERTIFICATE";
if (err == CERTIFICATE_REVOKED)
return "CERTIFICATE_REVOKED";
if (err == CERTIFICATE_EXPIRED)
return "CERTIFICATE_EXPIRED";
if (err == CERTIFICATE_UNKNOWN)
return "CERTIFICATE_UNKNOWN";
if (err == ILLEGAL_PARAMETER)
return "ILLEGAL_PARAMETER";
if (err == UNKNOWN_CA)
return "UNKNOWN_CA";
if (err == ACCESS_DENIED)
return "ACCESS_DENIED";
if (err == DECODE_ERROR)
return "DECODE_ERROR";
if (err == DECRYPT_ERROR)
return "DECRYPT_ERROR";
if (err == TOO_MANY_CIDS_REQUESTED)
return "TOO_MANY_CIDS_REQUESTED";
if (err == PROTOCOL_VERSION)
return "PROTOCOL_VERSION";
if (err == INSUFFICIENT_SECURITY)
return "INSUFFICIENT_SECURITY";
if (err == INTERNAL_ERROR)
return "INTERNAL_ERROR";
if (err == INAPPROPRIATE_FALLBACK)
return "INAPPROPRIATE_FALLBACK";
if (err == USER_CANCELED)
return "USER_CANCELED";
if (err == MISSING_EXTENSION)
return "MISSING_EXTENSION";
if (err == UNSUPPORTED_EXTENSION)
return "UNSUPPORTED_EXTENSION";
if (err == UNRECOGNIZED_NAME)
return "UNRECOGNIZED_NAME";
if (err == BAD_CERTIFICATE_STATUS_RESPONSE)
return "BAD_CERTIFICATE_STATUS_RESPONSE";
if (err == UNKNOWN_PSK_IDENTITY)
return "UNKNOWN_PSK_IDENTITY";
if (err == CERTIFICATE_REQUIRED)
return "CERTIFICATE_REQUIRED";
if (err == NO_APPLICATION_PROTOCOL)
return "NO_APPLICATION_PROTOCOL";
return "?? " + err;
}
}
}

View File

@ -35,6 +35,7 @@ public class QuicheConfig
private int version = Quiche.QUICHE_PROTOCOL_VERSION;
private Boolean verifyPeer;
private String trustedCertsPemPath;
private String certChainPemPath;
private String privKeyPemPath;
private String[] applicationProtos;
@ -65,6 +66,11 @@ public class QuicheConfig
return verifyPeer;
}
public String getTrustedCertsPemPath()
{
return trustedCertsPemPath;
}
public String getCertChainPemPath()
{
return certChainPemPath;
@ -150,6 +156,11 @@ public class QuicheConfig
this.verifyPeer = verify;
}
public void setTrustedCertsPemPath(String trustedCertsPemPath)
{
this.trustedCertsPemPath = trustedCertsPemPath;
}
public void setCertChainPemPath(String path)
{
this.certChainPemPath = path;

View File

@ -148,6 +148,8 @@ public abstract class QuicheConnection
public abstract CloseInfo getRemoteCloseInfo();
public abstract CloseInfo getLocalCloseInfo();
public static class CloseInfo
{
private final long error;

View File

@ -1,102 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.quic.quiche;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.util.Base64;
public class SSLKeyPair
{
private static final byte[] BEGIN_KEY = "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] END_KEY = "-----END PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] BEGIN_CERT = "-----BEGIN CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] END_CERT = "-----END CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(StandardCharsets.US_ASCII);
private static final int LINE_LENGTH = 64;
private final Base64.Encoder encoder = Base64.getMimeEncoder(LINE_LENGTH, LINE_SEPARATOR);
private final Key key;
private final Certificate[] certChain;
private final String alias;
public SSLKeyPair(Path storeFile, String storeType, char[] storePassword, String alias, char[] keyPassword) throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
{
KeyStore keyStore = KeyStore.getInstance(storeType);
try (InputStream is = Files.newInputStream(storeFile))
{
keyStore.load(is, storePassword);
this.alias = alias;
this.key = keyStore.getKey(alias, keyPassword);
this.certChain = keyStore.getCertificateChain(alias);
}
}
/**
* @return [0] is the key file, [1] is the cert file.
*/
public File[] export(File targetFolder) throws Exception
{
File[] files = new File[2];
files[0] = new File(targetFolder, alias + ".key");
files[1] = new File(targetFolder, alias + ".crt");
try (FileOutputStream fos = new FileOutputStream(files[0]))
{
writeAsPEM(fos, key);
}
try (FileOutputStream fos = new FileOutputStream(files[1]))
{
for (Certificate cert : certChain)
writeAsPEM(fos, cert);
}
return files;
}
private void writeAsPEM(OutputStream outputStream, Key key) throws IOException
{
byte[] encoded = encoder.encode(key.getEncoded());
outputStream.write(BEGIN_KEY);
outputStream.write(LINE_SEPARATOR);
outputStream.write(encoded);
outputStream.write(LINE_SEPARATOR);
outputStream.write(END_KEY);
outputStream.write(LINE_SEPARATOR);
}
private void writeAsPEM(OutputStream outputStream, Certificate certificate) throws CertificateEncodingException, IOException
{
byte[] encoded = encoder.encode(certificate.getEncoded());
outputStream.write(BEGIN_CERT);
outputStream.write(LINE_SEPARATOR);
outputStream.write(encoded);
outputStream.write(LINE_SEPARATOR);
outputStream.write(END_CERT);
outputStream.write(LINE_SEPARATOR);
}
}

View File

@ -28,6 +28,8 @@ import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import org.eclipse.jetty.quic.quiche.Quiche;
import org.eclipse.jetty.quic.quiche.Quiche.quic_error;
import org.eclipse.jetty.quic.quiche.Quiche.quiche_error;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
@ -148,7 +150,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
MemorySegment localSockaddr = sockaddr.convert(local, scope);
MemorySegment peerSockaddr = sockaddr.convert(peer, scope);
MemoryAddress quicheConn = quiche_h.quiche_connect(CLinker.toCString(peer.getHostName(), scope), scid, scid.byteSize(), localSockaddr, localSockaddr.byteSize(), peerSockaddr, peerSockaddr.byteSize(), libQuicheConfig);
MemoryAddress quicheConn = quiche_h.quiche_connect(CLinker.toCString(peer.getHostString(), scope), scid, scid.byteSize(), localSockaddr, localSockaddr.byteSize(), peerSockaddr, peerSockaddr.byteSize(), libQuicheConfig);
ForeignIncubatorQuicheConnection connection = new ForeignIncubatorQuicheConnection(quicheConn, libQuicheConfig, scope);
keepScope = true;
return connection;
@ -170,13 +172,29 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
if (verifyPeer != null)
quiche_h.quiche_config_verify_peer(quicheConfig, verifyPeer ? C_TRUE : C_FALSE);
String trustedCertsPemPath = config.getTrustedCertsPemPath();
if (trustedCertsPemPath != null)
{
int rc = quiche_h.quiche_config_load_verify_locations_from_file(quicheConfig, CLinker.toCString(trustedCertsPemPath, scope).address());
if (rc < 0)
throw new IOException("Error loading trusted certificates file " + trustedCertsPemPath + " : " + Quiche.quiche_error.errToString(rc));
}
String certChainPemPath = config.getCertChainPemPath();
if (certChainPemPath != null)
quiche_h.quiche_config_load_cert_chain_from_pem_file(quicheConfig, CLinker.toCString(certChainPemPath, scope).address());
{
int rc = quiche_h.quiche_config_load_cert_chain_from_pem_file(quicheConfig, CLinker.toCString(certChainPemPath, scope).address());
if (rc < 0)
throw new IOException("Error loading certificate chain file " + certChainPemPath + " : " + Quiche.quiche_error.errToString(rc));
}
String privKeyPemPath = config.getPrivKeyPemPath();
if (privKeyPemPath != null)
quiche_h.quiche_config_load_priv_key_from_pem_file(quicheConfig, CLinker.toCString(privKeyPemPath, scope).address());
{
int rc = quiche_h.quiche_config_load_priv_key_from_pem_file(quicheConfig, CLinker.toCString(privKeyPemPath, scope).address());
if (rc < 0)
throw new IOException("Error loading private key file " + privKeyPemPath + " : " + Quiche.quiche_error.errToString(rc));
}
String[] applicationProtos = config.getApplicationProtos();
if (applicationProtos != null)
@ -483,6 +501,31 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
}
}
public byte[] getPeerCertificate()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
MemorySegment outSegment = MemorySegment.allocateNative(CLinker.C_POINTER, scope);
MemorySegment outLenSegment = MemorySegment.allocateNative(CLinker.C_LONG, scope);
quiche_h.quiche_conn_peer_cert(quicheConn, outSegment.address(), outLenSegment.address());
long outLen = getLong(outLenSegment);
if (outLen == 0L)
return null;
byte[] out = new byte[(int)outLen];
// dereference outSegment pointer
MemoryAddress memoryAddress = MemoryAddress.ofLong(getLong(outSegment));
memoryAddress.asSegment(outLen, ResourceScope.globalScope()).asByteBuffer().get(out);
return out;
}
}
}
@Override
protected List<Long> iterableStreamIds(boolean write)
{
@ -541,8 +584,11 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
received = quiche_h.quiche_conn_recv(quicheConn, bufferSegment.address(), buffer.remaining(), recvInfo.address());
}
}
// If quiche_conn_recv() fails, quiche_conn_local_error() can be called to get the standard error.
if (received < 0)
throw new IOException("failed to receive packet; err=" + quiche_error.errToString(received));
throw new IOException("failed to receive packet;" +
" quiche_err=" + quiche_error.errToString(received) +
" quic_err=" + quic_error.errToString(getLocalCloseInfo().error()));
buffer.position((int)(buffer.position() + received));
return (int)received;
}
@ -579,7 +625,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
if (written == quiche_error.QUICHE_ERR_DONE)
return 0;
if (written < 0L)
throw new IOException("failed to send packet; err=" + quiche_error.errToString(written));
throw new IOException("failed to send packet; quiche_err=" + quiche_error.errToString(written));
buffer.position((int)(prevPosition + written));
return (int)written;
}
@ -762,7 +808,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
if (value < 0)
{
if (LOG.isDebugEnabled())
LOG.debug("could not read window capacity for stream {} err={}", streamId, quiche_error.errToString(value));
LOG.debug("could not read window capacity for stream {} quiche_err={}", streamId, quiche_error.errToString(value));
}
return value;
}
@ -821,7 +867,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
if (written == quiche_error.QUICHE_ERR_DONE)
return 0;
if (written < 0L)
throw new IOException("failed to write to stream " + streamId + "; err=" + quiche_error.errToString(written));
throw new IOException("failed to write to stream " + streamId + "; quiche_err=" + quiche_error.errToString(written));
buffer.position((int)(buffer.position() + written));
return (int)written;
}
@ -862,7 +908,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
if (read == quiche_error.QUICHE_ERR_DONE)
return isStreamFinished(streamId) ? -1 : 0;
if (read < 0L)
throw new IOException("failed to read from stream " + streamId + "; err=" + quiche_error.errToString(read));
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
buffer.position((int)(buffer.position() + read));
return (int)read;
}
@ -918,6 +964,45 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection
}
}
@Override
public CloseInfo getLocalCloseInfo()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
MemorySegment app = MemorySegment.allocateNative(CLinker.C_CHAR, scope);
MemorySegment error = MemorySegment.allocateNative(CLinker.C_LONG, scope);
MemorySegment reason = MemorySegment.allocateNative(CLinker.C_POINTER, scope);
MemorySegment reasonLength = MemorySegment.allocateNative(CLinker.C_LONG, scope);
if (quiche_h.quiche_conn_local_error(quicheConn, app.address(), error.address(), reason.address(), reasonLength.address()) != C_FALSE)
{
long errorValue = getLong(error);
long reasonLengthValue = getLong(reasonLength);
String reasonValue;
if (reasonLengthValue == 0L)
{
reasonValue = null;
}
else
{
byte[] reasonBytes = new byte[(int)reasonLengthValue];
// dereference reason pointer
MemoryAddress memoryAddress = MemoryAddress.ofLong(getLong(reason));
memoryAddress.asSegment(reasonLengthValue, ResourceScope.globalScope()).asByteBuffer().get(reasonBytes);
reasonValue = new String(reasonBytes, StandardCharsets.UTF_8);
}
return new CloseInfo(errorValue, reasonValue);
}
return null;
}
}
}
private static void putLong(MemorySegment memorySegment, long value)
{
memorySegment.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(value);

View File

@ -52,6 +52,12 @@ public class quiche_h
FunctionDescriptor.ofVoid(C_POINTER, C_INT)
);
private static final MethodHandle quiche_config_load_verify_locations_from_file$MH = downcallHandle(
"quiche_config_load_verify_locations_from_file",
"(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)I",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)
);
private static final MethodHandle quiche_config_load_cert_chain_from_pem_file$MH = downcallHandle(
"quiche_config_load_cert_chain_from_pem_file",
"(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)I",
@ -250,12 +256,24 @@ public class quiche_h
FunctionDescriptor.of(C_CHAR, C_POINTER)
);
private static final MethodHandle quiche_conn_peer_cert$MH = downcallHandle(
"quiche_conn_peer_cert",
"(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)V",
FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_POINTER)
);
private static final MethodHandle quiche_conn_peer_error$MH = downcallHandle(
"quiche_conn_peer_error",
"(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)B",
FunctionDescriptor.of(C_CHAR, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER)
);
private static final MethodHandle quiche_conn_local_error$MH = downcallHandle(
"quiche_conn_local_error",
"(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)B",
FunctionDescriptor.of(C_CHAR, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER)
);
private static final MethodHandle quiche_conn_stats$MH = downcallHandle(
"quiche_conn_stats",
"(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)V",
@ -364,6 +382,18 @@ public class quiche_h
}
}
public static int quiche_config_load_verify_locations_from_file(MemoryAddress config, MemoryAddress path)
{
try
{
return (int) quiche_config_load_verify_locations_from_file$MH.invokeExact(config, path);
}
catch (Throwable ex)
{
throw new AssertionError("should not reach here", ex);
}
}
public static int quiche_config_load_cert_chain_from_pem_file(MemoryAddress config, MemoryAddress path)
{
try
@ -688,6 +718,18 @@ public class quiche_h
}
}
public static void quiche_conn_peer_cert(MemoryAddress conn, MemoryAddress out, MemoryAddress out_len)
{
try
{
quiche_conn_peer_cert$MH.invokeExact(conn, out, out_len);
}
catch (Throwable ex)
{
throw new AssertionError("should not reach here", ex);
}
}
public static byte quiche_conn_peer_error(MemoryAddress conn, MemoryAddress is_app, MemoryAddress error_code, MemoryAddress reason, MemoryAddress reason_len)
{
try
@ -700,6 +742,18 @@ public class quiche_h
}
}
public static byte quiche_conn_local_error(MemoryAddress conn, MemoryAddress is_app, MemoryAddress error_code, MemoryAddress reason, MemoryAddress reason_len)
{
try
{
return (byte) quiche_conn_local_error$MH.invokeExact(conn, is_app, error_code, reason, reason_len);
}
catch (Throwable ex)
{
throw new AssertionError("should not reach here", ex);
}
}
public static long quiche_conn_stream_capacity(MemoryAddress conn, long stream_id)
{
try

View File

@ -0,0 +1,249 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.foreign.incubator;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import org.eclipse.jetty.quic.quiche.PemExporter;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
@ExtendWith(WorkDirExtension.class)
public class LowLevelQuicheClientCertTest
{
public WorkDir workDir;
private final Collection<ForeignIncubatorQuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private InetSocketAddress clientSocketAddress;
private InetSocketAddress serverSocketAddress;
private QuicheConfig clientQuicheConfig;
private QuicheConfig serverQuicheConfig;
private ForeignIncubatorQuicheConnection.TokenMinter tokenMinter;
private ForeignIncubatorQuicheConnection.TokenValidator tokenValidator;
private Certificate[] serverCertificateChain;
@BeforeEach
protected void setUp() throws Exception
{
clientSocketAddress = new InetSocketAddress("localhost", 9999);
serverSocketAddress = new InetSocketAddress("localhost", 8888);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = getClass().getResourceAsStream("/keystore.p12"))
{
keyStore.load(is, "storepwd".toCharArray());
}
Path targetFolder = workDir.getEmptyPathDir();
Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder);
Path trustStorePath = PemExporter.exportTrustStore(keyStore, targetFolder);
clientQuicheConfig = new QuicheConfig();
clientQuicheConfig.setApplicationProtos("http/0.9");
clientQuicheConfig.setDisableActiveMigration(true);
clientQuicheConfig.setPrivKeyPemPath(keyPair[0].toString());
clientQuicheConfig.setCertChainPemPath(keyPair[1].toString());
clientQuicheConfig.setVerifyPeer(true);
clientQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString());
clientQuicheConfig.setMaxIdleTimeout(1_000L);
clientQuicheConfig.setInitialMaxData(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataUni(10_000_000L);
clientQuicheConfig.setInitialMaxStreamsUni(100L);
clientQuicheConfig.setInitialMaxStreamsBidi(100L);
clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC);
serverCertificateChain = keyStore.getCertificateChain("mykey");
serverQuicheConfig = new QuicheConfig();
serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString());
serverQuicheConfig.setCertChainPemPath(keyPair[1].toString());
serverQuicheConfig.setApplicationProtos("http/0.9");
serverQuicheConfig.setVerifyPeer(true);
serverQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString());
serverQuicheConfig.setMaxIdleTimeout(1_000L);
serverQuicheConfig.setInitialMaxData(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataUni(10_000_000L);
serverQuicheConfig.setInitialMaxStreamsUni(100L);
serverQuicheConfig.setInitialMaxStreamsBidi(100L);
serverQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC);
tokenMinter = new TestTokenMinter();
tokenValidator = new TestTokenValidator();
}
@AfterEach
protected void tearDown()
{
connectionsToDisposeOf.forEach(ForeignIncubatorQuicheConnection::dispose);
connectionsToDisposeOf.clear();
}
@Test
public void testClientCert() throws Exception
{
// establish connection
Map.Entry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> entry = connectClientToServer();
ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey();
ForeignIncubatorQuicheConnection serverQuicheConnection = entry.getValue();
// assert that the client certificate was correctly received by the server
byte[] receivedClientCertificate = serverQuicheConnection.getPeerCertificate();
byte[] configuredClientCertificate = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(configuredClientCertificate, receivedClientCertificate), is(true));
// assert that the server certificate was correctly received by the client
byte[] receivedServerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] configuredServerCertificate = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(configuredServerCertificate, receivedServerCertificate), is(true));
}
private void drainServerToFeedClient(Map.Entry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> entry, int expectedSize) throws IOException
{
ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey();
ForeignIncubatorQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(expectedSize));
}
private void drainClientToFeedServer(Map.Entry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> entry, int expectedSize) throws IOException
{
ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey();
ForeignIncubatorQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = serverQuicheConnection.feedCipherBytes(buffer, serverSocketAddress, clientSocketAddress);
assertThat(fed, is(expectedSize));
}
private Map.Entry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> connectClientToServer() throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
ByteBuffer buffer2 = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
ForeignIncubatorQuicheConnection clientQuicheConnection = ForeignIncubatorQuicheConnection.connect(clientQuicheConfig, clientSocketAddress, serverSocketAddress);
connectionsToDisposeOf.add(clientQuicheConnection);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
ForeignIncubatorQuicheConnection serverQuicheConnection = ForeignIncubatorQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress);
assertThat(serverQuicheConnection, is(nullValue()));
boolean negotiated = ForeignIncubatorQuicheConnection.negotiate(tokenMinter, buffer, buffer2);
assertThat(negotiated, is(true));
buffer2.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer2, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(79));
buffer.clear();
drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
serverQuicheConnection = ForeignIncubatorQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress);
assertThat(serverQuicheConnection, is(not(nullValue())));
connectionsToDisposeOf.add(serverQuicheConnection);
buffer.clear();
drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(1200));
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(false));
AbstractMap.SimpleImmutableEntry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection);
int protosLen = 0;
for (String proto : clientQuicheConfig.getApplicationProtos())
protosLen += 1 + proto.getBytes(StandardCharsets.UTF_8).length;
// 1st round
drainServerToFeedClient(entry, 451 + protosLen);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
drainClientToFeedServer(entry, 1200);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
// 2nd round (needed b/c of client cert)
drainServerToFeedClient(entry, 71);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
drainClientToFeedServer(entry, 222);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(true));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
return entry;
}
private static class TestTokenMinter implements QuicheConnection.TokenMinter
{
@Override
public byte[] mint(byte[] dcid, int len)
{
return ByteBuffer.allocate(len).put(dcid, 0, len).array();
}
}
private static class TestTokenValidator implements QuicheConnection.TokenValidator
{
@Override
public byte[] validate(byte[] token, int len)
{
return ByteBuffer.allocate(len).put(token, 0, len).array();
}
}
}

View File

@ -13,27 +13,32 @@
package org.eclipse.jetty.quic.quiche.foreign.incubator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.quic.quiche.PemExporter;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.quic.quiche.SSLKeyPair;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN;
import static org.hamcrest.MatcherAssert.assertThat;
@ -43,8 +48,11 @@ import static org.hamcrest.core.Is.is;
// TODO: make this test work in Java 18 too.
@EnabledOnJre(value = JRE.JAVA_17, disabledReason = "Java 18's Foreign APIs are incompatible with Java 17's Foreign APIs")
@ExtendWith(WorkDirExtension.class)
public class LowLevelQuicheTest
{
public WorkDir workDir;
private final Collection<ForeignIncubatorQuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private InetSocketAddress clientSocketAddress;
@ -53,6 +61,7 @@ public class LowLevelQuicheTest
private QuicheConfig serverQuicheConfig;
private ForeignIncubatorQuicheConnection.TokenMinter tokenMinter;
private ForeignIncubatorQuicheConnection.TokenValidator tokenValidator;
private Certificate[] serverCertificateChain;
@BeforeEach
protected void setUp() throws Exception
@ -60,10 +69,18 @@ public class LowLevelQuicheTest
clientSocketAddress = new InetSocketAddress("localhost", 9999);
serverSocketAddress = new InetSocketAddress("localhost", 8888);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = getClass().getResourceAsStream("/keystore.p12"))
{
keyStore.load(is, "storepwd".toCharArray());
}
Path targetFolder = workDir.getEmptyPathDir();
clientQuicheConfig = new QuicheConfig();
clientQuicheConfig.setApplicationProtos("http/0.9");
clientQuicheConfig.setDisableActiveMigration(true);
clientQuicheConfig.setVerifyPeer(false);
clientQuicheConfig.setVerifyPeer(true);
clientQuicheConfig.setTrustedCertsPemPath(PemExporter.exportTrustStore(keyStore, targetFolder).toString());
clientQuicheConfig.setMaxIdleTimeout(1_000L);
clientQuicheConfig.setInitialMaxData(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
@ -73,11 +90,11 @@ public class LowLevelQuicheTest
clientQuicheConfig.setInitialMaxStreamsBidi(100L);
clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC);
SSLKeyPair serverKeyPair = new SSLKeyPair(Paths.get(Objects.requireNonNull(getClass().getResource("/keystore.p12")).toURI()), "PKCS12", "storepwd".toCharArray(), "mykey", "storepwd".toCharArray());
File[] pemFiles = serverKeyPair.export(new File(System.getProperty("java.io.tmpdir")));
serverCertificateChain = keyStore.getCertificateChain("mykey");
serverQuicheConfig = new QuicheConfig();
serverQuicheConfig.setPrivKeyPemPath(pemFiles[0].getPath());
serverQuicheConfig.setCertChainPemPath(pemFiles[1].getPath());
Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder);
serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString());
serverQuicheConfig.setCertChainPemPath(keyPair[1].toString());
serverQuicheConfig.setApplicationProtos("http/0.9");
serverQuicheConfig.setVerifyPeer(false);
serverQuicheConfig.setMaxIdleTimeout(1_000L);
@ -138,6 +155,14 @@ public class LowLevelQuicheTest
// assert that stream 0 is finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(true));
// assert that there is not client certificate
assertThat(serverQuicheConnection.getPeerCertificate(), nullValue());
// assert that the server certificate was correctly received by the client
byte[] peerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] serverCert = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(serverCert, peerCertificate), is(true));
}
@Test
@ -171,6 +196,14 @@ public class LowLevelQuicheTest
// assert that stream 0 is finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(true));
// assert that there is not client certificate
assertThat(serverQuicheConnection.getPeerCertificate(), nullValue());
// assert that the server certificate was correctly received by the client
byte[] peerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] serverCert = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(serverCert, peerCertificate), is(true));
}
@Test
@ -186,6 +219,14 @@ public class LowLevelQuicheTest
assertThat(clientQuicheConnection.getNegotiatedProtocol(), is(""));
assertThat(serverQuicheConnection.getNegotiatedProtocol(), is(""));
// assert that there is not client certificate
assertThat(serverQuicheConnection.getPeerCertificate(), nullValue());
// assert that the server certificate was correctly received by the client
byte[] peerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] serverCert = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(serverCert, peerCertificate), is(true));
}
private void drainServerToFeedClient(Map.Entry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> entry, int expectedSize) throws IOException
@ -261,7 +302,7 @@ public class LowLevelQuicheTest
for (String proto : clientQuicheConfig.getApplicationProtos())
protosLen += 1 + proto.getBytes(StandardCharsets.UTF_8).length;
drainServerToFeedClient(entry, 300 + protosLen);
drainServerToFeedClient(entry, 420 + protosLen);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));

View File

@ -22,6 +22,8 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.quic.quiche.Quiche;
import org.eclipse.jetty.quic.quiche.Quiche.quic_error;
import org.eclipse.jetty.quic.quiche.Quiche.quiche_error;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
@ -114,7 +116,7 @@ public class JnaQuicheConnection extends QuicheConnection
SizedStructure<sockaddr> localSockaddr = sockaddr.convert(local);
SizedStructure<sockaddr> peerSockaddr = sockaddr.convert(peer);
LibQuiche.quiche_conn quicheConn = LibQuiche.INSTANCE.quiche_connect(peer.getHostName(), scid, new size_t(scid.length), localSockaddr.getStructure(), localSockaddr.getSize(), peerSockaddr.getStructure(), peerSockaddr.getSize(), libQuicheConfig);
LibQuiche.quiche_conn quicheConn = LibQuiche.INSTANCE.quiche_connect(peer.getHostString(), scid, new size_t(scid.length), localSockaddr.getStructure(), localSockaddr.getSize(), peerSockaddr.getStructure(), peerSockaddr.getSize(), libQuicheConfig);
return new JnaQuicheConnection(quicheConn, libQuicheConfig);
}
@ -128,13 +130,29 @@ public class JnaQuicheConnection extends QuicheConnection
if (verifyPeer != null)
LibQuiche.INSTANCE.quiche_config_verify_peer(quicheConfig, verifyPeer);
String trustedCertsPemPath = config.getTrustedCertsPemPath();
if (trustedCertsPemPath != null)
{
int rc = LibQuiche.INSTANCE.quiche_config_load_verify_locations_from_file(quicheConfig, trustedCertsPemPath);
if (rc != 0)
throw new IOException("Error loading trusted certificates file " + trustedCertsPemPath + " : " + Quiche.quiche_error.errToString(rc));
}
String certChainPemPath = config.getCertChainPemPath();
if (certChainPemPath != null)
LibQuiche.INSTANCE.quiche_config_load_cert_chain_from_pem_file(quicheConfig, certChainPemPath);
{
int rc = LibQuiche.INSTANCE.quiche_config_load_cert_chain_from_pem_file(quicheConfig, certChainPemPath);
if (rc < 0)
throw new IOException("Error loading certificate chain file " + certChainPemPath + " : " + Quiche.quiche_error.errToString(rc));
}
String privKeyPemPath = config.getPrivKeyPemPath();
if (privKeyPemPath != null)
LibQuiche.INSTANCE.quiche_config_load_priv_key_from_pem_file(quicheConfig, privKeyPemPath);
{
int rc = LibQuiche.INSTANCE.quiche_config_load_priv_key_from_pem_file(quicheConfig, privKeyPemPath);
if (rc < 0)
throw new IOException("Error loading private key file " + privKeyPemPath + " : " + Quiche.quiche_error.errToString(rc));
}
String[] applicationProtos = config.getApplicationProtos();
if (applicationProtos != null)
@ -385,8 +403,29 @@ public class JnaQuicheConnection extends QuicheConnection
public void enableQlog(String filename, String title, String desc) throws IOException
{
if (!LibQuiche.INSTANCE.quiche_conn_set_qlog_path(quicheConn, filename, title, desc))
throw new IOException("unable to set qlog path to " + filename);
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
if (!LibQuiche.INSTANCE.quiche_conn_set_qlog_path(quicheConn, filename, title, desc))
throw new IOException("unable to set qlog path to " + filename);
}
}
public byte[] getPeerCertificate()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
char_pointer out = new char_pointer();
size_t_pointer out_len = new size_t_pointer();
LibQuiche.INSTANCE.quiche_conn_peer_cert(quicheConn, out, out_len);
int len = out_len.getPointee().intValue();
return out.getValueAsBytes(len);
}
}
@Override
@ -429,9 +468,12 @@ public class JnaQuicheConnection extends QuicheConnection
SizedStructure<sockaddr> peerSockaddr = sockaddr.convert(peer);
info.from = peerSockaddr.getStructure().byReference();
info.from_len = peerSockaddr.getSize();
// If quiche_conn_recv() fails, quiche_conn_local_error() can be called to get the standard error.
int received = LibQuiche.INSTANCE.quiche_conn_recv(quicheConn, buffer, new size_t(buffer.remaining()), info).intValue();
if (received < 0)
throw new IOException("failed to receive packet; err=" + quiche_error.errToString(received));
throw new IOException("failed to receive packet;" +
" quiche_err=" + quiche_error.errToString(received) +
" quic_err=" + quic_error.errToString(getLocalCloseInfo().error()));
buffer.position(buffer.position() + received);
return received;
}
@ -459,7 +501,7 @@ public class JnaQuicheConnection extends QuicheConnection
if (written == quiche_error.QUICHE_ERR_DONE)
return 0;
if (written < 0L)
throw new IOException("failed to send packet; err=" + quiche_error.errToString(written));
throw new IOException("failed to send packet; quiche_err=" + quiche_error.errToString(written));
int prevPosition = buffer.position();
buffer.position(prevPosition + written);
return written;
@ -619,7 +661,7 @@ public class JnaQuicheConnection extends QuicheConnection
if (value < 0)
{
if (LOG.isDebugEnabled())
LOG.debug("could not read window capacity for stream {} err={}", streamId, quiche_error.errToString(value));
LOG.debug("could not read window capacity for stream {} quiche_err={}", streamId, quiche_error.errToString(value));
}
return value;
}
@ -651,7 +693,7 @@ public class JnaQuicheConnection extends QuicheConnection
if (written == quiche_error.QUICHE_ERR_DONE)
return 0;
if (written < 0L)
throw new IOException("failed to write to stream " + streamId + "; err=" + quiche_error.errToString(written));
throw new IOException("failed to write to stream " + streamId + "; quiche_err=" + quiche_error.errToString(written));
buffer.position(buffer.position() + written);
return written;
}
@ -669,7 +711,7 @@ public class JnaQuicheConnection extends QuicheConnection
if (read == quiche_error.QUICHE_ERR_DONE)
return isStreamFinished(streamId) ? -1 : 0;
if (read < 0L)
throw new IOException("failed to read from stream " + streamId + "; err=" + quiche_error.errToString(read));
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
buffer.position(buffer.position() + read);
return read;
}
@ -702,4 +744,21 @@ public class JnaQuicheConnection extends QuicheConnection
return null;
}
}
@Override
public CloseInfo getLocalCloseInfo()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
bool_pointer app = new bool_pointer();
uint64_t_pointer error = new uint64_t_pointer();
char_pointer reason = new char_pointer();
size_t_pointer reasonLength = new size_t_pointer();
if (LibQuiche.INSTANCE.quiche_conn_local_error(quicheConn, app, error, reason, reasonLength))
return new CloseInfo(error.getValue(), reason.getValueAsString((int)reasonLength.getValue(), LibQuiche.CHARSET));
return null;
}
}
}

View File

@ -86,6 +86,9 @@ public interface LibQuiche extends Library
// Configures whether to verify the peer's certificate.
void quiche_config_verify_peer(quiche_config config, boolean v);
// Specifies a file where trusted CA certificates are stored for the purposes of certificate verification.
int quiche_config_load_verify_locations_from_file(quiche_config config, String path);
// Configures the list of supported application protocols.
int quiche_config_set_application_protos(quiche_config config, byte[] protos, size_t protos_len);
@ -425,6 +428,9 @@ public interface LibQuiche extends Library
// Returns true if the connection was closed due to the idle timeout.
boolean quiche_conn_is_timed_out(quiche_conn conn);
// Returns the peer's leaf certificate (if any) as a DER-encoded buffer.
void quiche_conn_peer_cert(quiche_conn conn, char_pointer out, size_t_pointer out_len);
// Returns true if a connection error was received, and updates the provided
// parameters accordingly.
boolean quiche_conn_peer_error(quiche_conn conn,

View File

@ -15,12 +15,24 @@ package org.eclipse.jetty.quic.quiche.jna;
import java.nio.charset.Charset;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.PointerByReference;
public class char_pointer extends PointerByReference
{
public String getValueAsString(int len, Charset charset)
{
return new String(getValue().getByteArray(0, len), charset);
Pointer value = getValue();
if (value == null)
return null;
return new String(value.getByteArray(0, len), charset);
}
public byte[] getValueAsBytes(int len)
{
Pointer value = getValue();
if (value == null)
return null;
return value.getByteArray(0, len);
}
}

View File

@ -0,0 +1,248 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.jna;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import org.eclipse.jetty.quic.quiche.PemExporter;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
@ExtendWith(WorkDirExtension.class)
public class LowLevelQuicheClientCertTest
{
public WorkDir workDir;
private final Collection<JnaQuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private InetSocketAddress clientSocketAddress;
private InetSocketAddress serverSocketAddress;
private QuicheConfig clientQuicheConfig;
private QuicheConfig serverQuicheConfig;
private JnaQuicheConnection.TokenMinter tokenMinter;
private JnaQuicheConnection.TokenValidator tokenValidator;
private Certificate[] serverCertificateChain;
@BeforeEach
protected void setUp() throws Exception
{
clientSocketAddress = new InetSocketAddress("localhost", 9999);
serverSocketAddress = new InetSocketAddress("localhost", 8888);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = getClass().getResourceAsStream("/keystore.p12"))
{
keyStore.load(is, "storepwd".toCharArray());
}
Path targetFolder = workDir.getEmptyPathDir();
Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder);
Path trustStorePath = PemExporter.exportTrustStore(keyStore, targetFolder);
clientQuicheConfig = new QuicheConfig();
clientQuicheConfig.setApplicationProtos("http/0.9");
clientQuicheConfig.setDisableActiveMigration(true);
clientQuicheConfig.setPrivKeyPemPath(keyPair[0].toString());
clientQuicheConfig.setCertChainPemPath(keyPair[1].toString());
clientQuicheConfig.setVerifyPeer(true);
clientQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString());
clientQuicheConfig.setMaxIdleTimeout(1_000L);
clientQuicheConfig.setInitialMaxData(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataUni(10_000_000L);
clientQuicheConfig.setInitialMaxStreamsUni(100L);
clientQuicheConfig.setInitialMaxStreamsBidi(100L);
clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC);
serverCertificateChain = keyStore.getCertificateChain("mykey");
serverQuicheConfig = new QuicheConfig();
serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString());
serverQuicheConfig.setCertChainPemPath(keyPair[1].toString());
serverQuicheConfig.setApplicationProtos("http/0.9");
serverQuicheConfig.setVerifyPeer(true);
serverQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString());
serverQuicheConfig.setMaxIdleTimeout(1_000L);
serverQuicheConfig.setInitialMaxData(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataUni(10_000_000L);
serverQuicheConfig.setInitialMaxStreamsUni(100L);
serverQuicheConfig.setInitialMaxStreamsBidi(100L);
serverQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC);
tokenMinter = new TestTokenMinter();
tokenValidator = new TestTokenValidator();
}
@AfterEach
protected void tearDown()
{
connectionsToDisposeOf.forEach(JnaQuicheConnection::dispose);
connectionsToDisposeOf.clear();
}
@Test
public void testClientCert() throws Exception
{
// establish connection
Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry = connectClientToServer();
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
// assert that the client certificate was correctly received by the server
byte[] receivedClientCertificate = serverQuicheConnection.getPeerCertificate();
byte[] configuredClientCertificate = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(configuredClientCertificate, receivedClientCertificate), is(true));
// assert that the server certificate was correctly received by the client
byte[] receivedServerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] configuredServerCertificate = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(configuredServerCertificate, receivedServerCertificate), is(true));
}
private void drainServerToFeedClient(Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry, int expectedSize) throws IOException
{
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(expectedSize));
}
private void drainClientToFeedServer(Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry, int expectedSize) throws IOException
{
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = serverQuicheConnection.feedCipherBytes(buffer, serverSocketAddress, clientSocketAddress);
assertThat(fed, is(expectedSize));
}
private Map.Entry<JnaQuicheConnection, JnaQuicheConnection> connectClientToServer() throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
ByteBuffer buffer2 = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
JnaQuicheConnection clientQuicheConnection = JnaQuicheConnection.connect(clientQuicheConfig, clientSocketAddress, serverSocketAddress);
connectionsToDisposeOf.add(clientQuicheConnection);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
JnaQuicheConnection serverQuicheConnection = JnaQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress);
assertThat(serverQuicheConnection, is(nullValue()));
boolean negotiated = JnaQuicheConnection.negotiate(tokenMinter, buffer, buffer2);
assertThat(negotiated, is(true));
buffer2.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer2, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(79));
buffer.clear();
drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
serverQuicheConnection = JnaQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress);
assertThat(serverQuicheConnection, is(not(nullValue())));
connectionsToDisposeOf.add(serverQuicheConnection);
buffer.clear();
drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(1200));
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(false));
AbstractMap.SimpleImmutableEntry<JnaQuicheConnection, JnaQuicheConnection> entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection);
int protosLen = 0;
for (String proto : clientQuicheConfig.getApplicationProtos())
protosLen += 1 + proto.getBytes(LibQuiche.CHARSET).length;
// 1st round
drainServerToFeedClient(entry, 451 + protosLen);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
drainClientToFeedServer(entry, 1200);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
// 2nd round (needed b/c of client cert)
drainServerToFeedClient(entry, 72);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
drainClientToFeedServer(entry, 222);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(true));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
return entry;
}
private static class TestTokenMinter implements QuicheConnection.TokenMinter
{
@Override
public byte[] mint(byte[] dcid, int len)
{
return ByteBuffer.allocate(len).put(dcid, 0, len).array();
}
}
private static class TestTokenValidator implements QuicheConnection.TokenValidator
{
@Override
public byte[] validate(byte[] token, int len)
{
return ByteBuffer.allocate(len).put(token, 0, len).array();
}
}
}

View File

@ -13,24 +13,29 @@
package org.eclipse.jetty.quic.quiche.jna;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.quic.quiche.PemExporter;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.quic.quiche.SSLKeyPair;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN;
import static org.hamcrest.MatcherAssert.assertThat;
@ -38,8 +43,11 @@ import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
@ExtendWith(WorkDirExtension.class)
public class LowLevelQuicheTest
{
public WorkDir workDir;
private final Collection<JnaQuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private InetSocketAddress clientSocketAddress;
@ -48,6 +56,7 @@ public class LowLevelQuicheTest
private QuicheConfig serverQuicheConfig;
private JnaQuicheConnection.TokenMinter tokenMinter;
private JnaQuicheConnection.TokenValidator tokenValidator;
private Certificate[] serverCertificateChain;
@BeforeEach
protected void setUp() throws Exception
@ -55,10 +64,18 @@ public class LowLevelQuicheTest
clientSocketAddress = new InetSocketAddress("localhost", 9999);
serverSocketAddress = new InetSocketAddress("localhost", 8888);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = getClass().getResourceAsStream("/keystore.p12"))
{
keyStore.load(is, "storepwd".toCharArray());
}
Path targetFolder = workDir.getEmptyPathDir();
clientQuicheConfig = new QuicheConfig();
clientQuicheConfig.setApplicationProtos("http/0.9");
clientQuicheConfig.setDisableActiveMigration(true);
clientQuicheConfig.setVerifyPeer(false);
clientQuicheConfig.setVerifyPeer(true);
clientQuicheConfig.setTrustedCertsPemPath(PemExporter.exportTrustStore(keyStore, targetFolder).toString());
clientQuicheConfig.setMaxIdleTimeout(1_000L);
clientQuicheConfig.setInitialMaxData(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
@ -68,11 +85,11 @@ public class LowLevelQuicheTest
clientQuicheConfig.setInitialMaxStreamsBidi(100L);
clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC);
SSLKeyPair serverKeyPair = new SSLKeyPair(Paths.get(Objects.requireNonNull(getClass().getResource("/keystore.p12")).toURI()), "PKCS12", "storepwd".toCharArray(), "mykey", "storepwd".toCharArray());
File[] pemFiles = serverKeyPair.export(new File(System.getProperty("java.io.tmpdir")));
serverCertificateChain = keyStore.getCertificateChain("mykey");
serverQuicheConfig = new QuicheConfig();
serverQuicheConfig.setPrivKeyPemPath(pemFiles[0].getPath());
serverQuicheConfig.setCertChainPemPath(pemFiles[1].getPath());
Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder);
serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString());
serverQuicheConfig.setCertChainPemPath(keyPair[1].toString());
serverQuicheConfig.setApplicationProtos("http/0.9");
serverQuicheConfig.setVerifyPeer(false);
serverQuicheConfig.setMaxIdleTimeout(1_000L);
@ -133,6 +150,14 @@ public class LowLevelQuicheTest
// assert that stream 0 is finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(true));
// assert that there is not client certificate
assertThat(serverQuicheConnection.getPeerCertificate(), nullValue());
// assert that the server certificate was correctly received by the client
byte[] peerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] serverCert = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(serverCert, peerCertificate), is(true));
}
@Test
@ -166,6 +191,14 @@ public class LowLevelQuicheTest
// assert that stream 0 is finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(true));
// assert that there is not client certificate
assertThat(serverQuicheConnection.getPeerCertificate(), nullValue());
// assert that the server certificate was correctly received by the client
byte[] peerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] serverCert = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(serverCert, peerCertificate), is(true));
}
@Test
@ -181,6 +214,14 @@ public class LowLevelQuicheTest
assertThat(clientQuicheConnection.getNegotiatedProtocol(), is(""));
assertThat(serverQuicheConnection.getNegotiatedProtocol(), is(""));
// assert that there is not client certificate
assertThat(serverQuicheConnection.getPeerCertificate(), nullValue());
// assert that the server certificate was correctly received by the client
byte[] peerCertificate = clientQuicheConnection.getPeerCertificate();
byte[] serverCert = serverCertificateChain[0].getEncoded();
assertThat(Arrays.equals(serverCert, peerCertificate), is(true));
}
private void drainServerToFeedClient(Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry, int expectedSize) throws IOException
@ -256,7 +297,7 @@ public class LowLevelQuicheTest
for (String proto : clientQuicheConfig.getApplicationProtos())
protosLen += 1 + proto.getBytes(LibQuiche.CHARSET).length;
drainServerToFeedClient(entry, 300 + protosLen);
drainServerToFeedClient(entry, 420 + protosLen);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));

View File

@ -13,13 +13,14 @@
package org.eclipse.jetty.quic.server;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.EventListener;
import java.util.List;
import java.util.Set;
@ -36,8 +37,8 @@ import org.eclipse.jetty.quic.common.QuicConfiguration;
import org.eclipse.jetty.quic.common.QuicSession;
import org.eclipse.jetty.quic.common.QuicSessionContainer;
import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
import org.eclipse.jetty.quic.quiche.PemExporter;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.SSLKeyPair;
import org.eclipse.jetty.server.AbstractNetworkConnector;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Server;
@ -60,8 +61,9 @@ public class QuicServerConnector extends AbstractNetworkConnector
private final QuicSessionContainer container = new QuicSessionContainer();
private final ServerDatagramSelectorManager selectorManager;
private final SslContextFactory.Server sslContextFactory;
private File privateKeyFile;
private File certificateChainFile;
private Path privateKeyPemPath;
private Path certificateChainPemPath;
private Path trustedCertificatesPemPath;
private volatile DatagramChannel datagramChannel;
private volatile int localPort = -1;
private int inputBufferSize = 2048;
@ -89,7 +91,6 @@ public class QuicServerConnector extends AbstractNetworkConnector
// One bidirectional stream to simulate the TCP stream, and no unidirectional streams.
quicConfiguration.setMaxBidirectionalRemoteStreams(1);
quicConfiguration.setMaxUnidirectionalRemoteStreams(0);
quicConfiguration.setVerifyPeerCertificates(false);
}
public QuicConfiguration getQuicConfiguration()
@ -163,19 +164,32 @@ public class QuicServerConnector extends AbstractNetworkConnector
throw new IllegalStateException("Invalid KeyStore: no aliases");
String alias = sslContextFactory.getCertAlias();
if (alias == null)
alias = aliases.stream().findFirst().orElse("mykey");
char[] keyStorePassword = sslContextFactory.getKeyStorePassword().toCharArray();
alias = aliases.stream().findFirst().orElseThrow();
String keyManagerPassword = sslContextFactory.getKeyManagerPassword();
SSLKeyPair keyPair = new SSLKeyPair(
sslContextFactory.getKeyStoreResource().getPath(),
sslContextFactory.getKeyStoreType(),
keyStorePassword,
alias,
keyManagerPassword == null ? keyStorePassword : keyManagerPassword.toCharArray()
);
File[] pemFiles = keyPair.export(new File(System.getProperty("java.io.tmpdir")));
privateKeyFile = pemFiles[0];
certificateChainFile = pemFiles[1];
char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray();
KeyStore keyStore = sslContextFactory.getKeyStore();
Path certificateWorkPath = findPemWorkDirectory();
Path[] keyPair = PemExporter.exportKeyPair(keyStore, alias, password, certificateWorkPath);
privateKeyPemPath = keyPair[0];
certificateChainPemPath = keyPair[1];
KeyStore trustStore = sslContextFactory.getTrustStore();
if (trustStore != null)
trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, certificateWorkPath);
}
private Path findPemWorkDirectory()
{
Path pemWorkDirectory = getQuicConfiguration().getPemWorkDirectory();
if (pemWorkDirectory != null)
return pemWorkDirectory;
String jettyBase = System.getProperty("jetty.base");
if (jettyBase != null)
{
pemWorkDirectory = Path.of(jettyBase).resolve("work");
if (Files.exists(pemWorkDirectory))
return pemWorkDirectory;
}
throw new IllegalStateException("No PEM work directory configured");
}
@Override
@ -211,9 +225,10 @@ public class QuicServerConnector extends AbstractNetworkConnector
QuicheConfig newQuicheConfig()
{
QuicheConfig quicheConfig = new QuicheConfig();
quicheConfig.setPrivKeyPemPath(privateKeyFile.getPath());
quicheConfig.setCertChainPemPath(certificateChainFile.getPath());
quicheConfig.setVerifyPeer(quicConfiguration.isVerifyPeerCertificates());
quicheConfig.setPrivKeyPemPath(privateKeyPemPath.toString());
quicheConfig.setCertChainPemPath(certificateChainPemPath.toString());
quicheConfig.setTrustedCertsPemPath(trustedCertificatesPemPath == null ? null : trustedCertificatesPemPath.toString());
quicheConfig.setVerifyPeer(sslContextFactory.getNeedClientAuth() || sslContextFactory.getWantClientAuth());
// Idle timeouts must not be managed by Quiche.
quicheConfig.setMaxIdleTimeout(0L);
quicheConfig.setInitialMaxData((long)quicConfiguration.getSessionRecvWindow());
@ -240,8 +255,12 @@ public class QuicServerConnector extends AbstractNetworkConnector
@Override
protected void doStop() throws Exception
{
deleteFile(privateKeyFile);
deleteFile(certificateChainFile);
deleteFile(privateKeyPemPath);
privateKeyPemPath = null;
deleteFile(certificateChainPemPath);
certificateChainPemPath = null;
deleteFile(trustedCertificatesPemPath);
trustedCertificatesPemPath = null;
// We want the DatagramChannel to be stopped by the SelectorManager.
super.doStop();
@ -254,12 +273,12 @@ public class QuicServerConnector extends AbstractNetworkConnector
selectorManager.removeEventListener(l);
}
private void deleteFile(File file)
private void deleteFile(Path file)
{
try
{
if (file != null)
Files.delete(file.toPath());
Files.delete(file);
}
catch (IOException x)
{

View File

@ -13,9 +13,11 @@
package org.eclipse.jetty.test.client.transport;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
@ -143,16 +145,26 @@ public class AbstractTest
return new Server(serverThreads, null, new ArrayByteBufferPool());
}
protected SslContextFactory.Server newSslContextFactoryServer()
protected SslContextFactory.Server newSslContextFactoryServer() throws Exception
{
SslContextFactory.Server ssl = new SslContextFactory.Server();
ssl.setKeyStorePath("src/test/resources/keystore.p12");
ssl.setKeyStorePassword("storepwd");
ssl.setUseCipherSuitesOrder(true);
ssl.setCipherComparator(HTTP2Cipher.COMPARATOR);
configureSslContextFactory(ssl);
return ssl;
}
private void configureSslContextFactory(SslContextFactory sslContextFactory) throws Exception
{
KeyStore keystore = KeyStore.getInstance("PKCS12");
try (InputStream is = Files.newInputStream(Path.of("src/test/resources/keystore.p12")))
{
keystore.load(is, "storepwd".toCharArray());
}
sslContextFactory.setKeyStore(keystore);
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setUseCipherSuitesOrder(true);
sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
}
protected void startClient(Transport transport) throws Exception
{
QueuedThreadPool clientThreads = new QueuedThreadPool();
@ -174,11 +186,13 @@ public class AbstractTest
case FCGI:
yield new ServerConnector(server, 1, 1, newServerConnectionFactory(transport));
case H3:
yield new HTTP3ServerConnector(server, sslContextFactoryServer, newServerConnectionFactory(transport));
HTTP3ServerConnector h3Connector = new HTTP3ServerConnector(server, sslContextFactoryServer, newServerConnectionFactory(transport));
h3Connector.getQuicConfiguration().setPemWorkDirectory(Path.of(System.getProperty("java.io.tmpdir")));
yield h3Connector;
case UNIX_DOMAIN:
UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, newServerConnectionFactory(transport));
connector.setUnixDomainPath(unixDomainPath);
yield connector;
UnixDomainServerConnector unixConnector = new UnixDomainServerConnector(server, 1, 1, newServerConnectionFactory(transport));
unixConnector.setUnixDomainPath(unixDomainPath);
yield unixConnector;
};
}
@ -219,16 +233,15 @@ public class AbstractTest
return list.toArray(ConnectionFactory[]::new);
}
protected SslContextFactory.Client newSslContextFactoryClient()
protected SslContextFactory.Client newSslContextFactoryClient() throws Exception
{
SslContextFactory.Client ssl = new SslContextFactory.Client();
ssl.setKeyStorePath("src/test/resources/keystore.p12");
ssl.setKeyStorePassword("storepwd");
configureSslContextFactory(ssl);
ssl.setEndpointIdentificationAlgorithm(null);
return ssl;
}
protected HttpClientTransport newHttpClientTransport(Transport transport)
protected HttpClientTransport newHttpClientTransport(Transport transport) throws Exception
{
return switch (transport)
{
@ -253,7 +266,6 @@ public class AbstractTest
ClientConnector clientConnector = http3Client.getClientConnector();
clientConnector.setSelectors(1);
clientConnector.setSslContextFactory(newSslContextFactoryClient());
http3Client.getQuicConfiguration().setVerifyPeerCertificates(false);
yield new HttpClientTransportOverHTTP3(http3Client);
}
case FCGI -> new HttpClientTransportOverFCGI(1, "");

View File

@ -105,7 +105,7 @@ public class HttpChannelAssociationTest extends AbstractTest
assertTrue(latch.await(5 * idleTimeout, TimeUnit.MILLISECONDS));
}
private HttpClientTransport newHttpClientTransport(Transport transport, Predicate<HttpExchange> code)
private HttpClientTransport newHttpClientTransport(Transport transport, Predicate<HttpExchange> code) throws Exception
{
return switch (transport)
{
@ -173,7 +173,6 @@ public class HttpChannelAssociationTest extends AbstractTest
HTTP3Client http3Client = new HTTP3Client();
http3Client.getClientConnector().setSelectors(1);
http3Client.getClientConnector().setSslContextFactory(newSslContextFactoryClient());
http3Client.getQuicConfiguration().setVerifyPeerCertificates(false);
yield new HttpClientTransportOverHTTP3(http3Client)
{
@Override

View File

@ -43,7 +43,6 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.NetworkConnector;
@ -353,15 +352,10 @@ public class HttpClientTest extends AbstractTest
client.stop();
client.setSslContextFactory(clientTLS);
client.start();
if (transport == Transport.H3)
{
assumeTrue(false, "certificate verification not yet supported in quic");
// TODO: the lines below should be enough, but they don't work. To be investigated.
HttpClientTransportOverHTTP3 http3Transport = (HttpClientTransportOverHTTP3)client.getTransport();
http3Transport.getHTTP3Client().getQuicConfiguration().setVerifyPeerCertificates(true);
}
assertThrows(ExecutionException.class, () ->
// H3 times out b/c it is QUIC's way of figuring out a connection cannot be established.
Class<? extends Exception> expectedType = transport == Transport.H3 ? TimeoutException.class : ExecutionException.class;
assertThrows(expectedType, () ->
{
// Use an IP address not present in the certificate.
int serverPort = newURI(transport).getPort();

View File

@ -249,7 +249,6 @@ public class AbstractTest
ClientConnector clientConnector = http3Client.getClientConnector();
clientConnector.setSelectors(1);
clientConnector.setSslContextFactory(newSslContextFactoryClient());
http3Client.getQuicConfiguration().setVerifyPeerCertificates(false);
yield new HttpClientTransportOverHTTP3(http3Client);
}
case FCGI -> new HttpClientTransportOverFCGI(1, "");

View File

@ -248,7 +248,6 @@ public class AbstractTest
ClientConnector clientConnector = http3Client.getClientConnector();
clientConnector.setSelectors(1);
clientConnector.setSslContextFactory(newSslContextFactoryClient());
http3Client.getQuicConfiguration().setVerifyPeerCertificates(false);
yield new HttpClientTransportOverHTTP3(http3Client);
}
case FCGI -> new HttpClientTransportOverFCGI(1, "");

View File

@ -52,7 +52,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
@ -1106,7 +1105,7 @@ public class DistributionTests extends AbstractJettyHomeTest
assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
HTTP3Client http3Client = new HTTP3Client();
http3Client.getQuicConfiguration().setVerifyPeerCertificates(false);
http3Client.getClientConnector().setSslContextFactory(new SslContextFactory.Client(true));
this.client = new HttpClient(new HttpClientTransportOverHTTP3(http3Client));
this.client.start();
ContentResponse response = this.client.newRequest("localhost", h3Port)