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:
parent
476145b817
commit
8c4e75bf8d
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
Binary file not shown.
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -12,6 +12,7 @@ experimental
|
|||
http2
|
||||
jna
|
||||
quiche
|
||||
work
|
||||
|
||||
[lib]
|
||||
lib/http3/*.jar
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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()
|
||||
|
|
Binary file not shown.
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -148,6 +148,8 @@ public abstract class QuicheConnection
|
|||
|
||||
public abstract CloseInfo getRemoteCloseInfo();
|
||||
|
||||
public abstract CloseInfo getLocalCloseInfo();
|
||||
|
||||
public static class CloseInfo
|
||||
{
|
||||
private final long error;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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)
|
||||
{
|
||||
|
|
Binary file not shown.
|
@ -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, "");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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, "");
|
||||
|
|
Binary file not shown.
|
@ -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, "");
|
||||
|
|
Binary file not shown.
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue