start quic client plumbing
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
parent
fe5c65820d
commit
e6cd0bae64
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.http3</groupId>
|
||||
<artifactId>http3-parent</artifactId>
|
||||
<version>10.0.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>http3-client</artifactId>
|
||||
<name>Jetty :: HTTP3 :: Client</name>
|
||||
|
||||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.client</bundle-symbolic-name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http3</groupId>
|
||||
<artifactId>http3-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http3</groupId>
|
||||
<artifactId>http3-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-slf4j-impl</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,439 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.client;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.CancelledKeyException;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jetty.io.AbstractEndPoint;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.io.ManagedSelector;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.util.thread.Invocable;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ClientDatagramEndPoint extends AbstractEndPoint implements ManagedSelector.Selectable
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientDatagramEndPoint.class);
|
||||
|
||||
public static InetAddressArgument INET_ADDRESS_ARGUMENT = new InetAddressArgument();
|
||||
|
||||
private final AutoLock _lock = new AutoLock();
|
||||
private final DatagramChannel _channel;
|
||||
private final ManagedSelector _selector;
|
||||
private SelectionKey _key;
|
||||
private boolean _updatePending;
|
||||
// The current value for interestOps.
|
||||
private int _currentInterestOps = SelectionKey.OP_READ; // See DatagramReader.update()
|
||||
// The desired value for interestOps.
|
||||
private int _desiredInterestOps;
|
||||
|
||||
private abstract class RunnableTask implements Runnable, Invocable
|
||||
{
|
||||
final String _operation;
|
||||
|
||||
protected RunnableTask(String op)
|
||||
{
|
||||
_operation = op;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s:%s:%s", this, _operation, getInvocationType());
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class RunnableCloseable extends RunnableTask implements Closeable
|
||||
{
|
||||
protected RunnableCloseable(String op)
|
||||
{
|
||||
super(op);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.close();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.warn("Unable to close {}", this, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ManagedSelector.SelectorUpdate _updateKeyAction = this::updateKeyAction;
|
||||
|
||||
private final Runnable _runFillable = new RunnableCloseable("runFillable")
|
||||
{
|
||||
@Override
|
||||
public Invocable.InvocationType getInvocationType()
|
||||
{
|
||||
return getFillInterest().getCallbackInvocationType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
getFillInterest().fillable();
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable _runCompleteWrite = new RunnableCloseable("runCompleteWrite")
|
||||
{
|
||||
@Override
|
||||
public Invocable.InvocationType getInvocationType()
|
||||
{
|
||||
return getWriteFlusher().getCallbackInvocationType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
getWriteFlusher().completeWrite();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s:%s:%s->%s", this, _operation, getInvocationType(), getWriteFlusher());
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable _runCompleteWriteFillable = new RunnableCloseable("runCompleteWriteFillable")
|
||||
{
|
||||
@Override
|
||||
public Invocable.InvocationType getInvocationType()
|
||||
{
|
||||
Invocable.InvocationType fillT = getFillInterest().getCallbackInvocationType();
|
||||
Invocable.InvocationType flushT = getWriteFlusher().getCallbackInvocationType();
|
||||
if (fillT == flushT)
|
||||
return fillT;
|
||||
|
||||
if (fillT == Invocable.InvocationType.EITHER && flushT == Invocable.InvocationType.NON_BLOCKING)
|
||||
return Invocable.InvocationType.EITHER;
|
||||
|
||||
if (fillT == Invocable.InvocationType.NON_BLOCKING && flushT == Invocable.InvocationType.EITHER)
|
||||
return Invocable.InvocationType.EITHER;
|
||||
|
||||
return Invocable.InvocationType.BLOCKING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
getWriteFlusher().completeWrite();
|
||||
getFillInterest().fillable();
|
||||
}
|
||||
};
|
||||
|
||||
public ClientDatagramEndPoint(DatagramChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
|
||||
{
|
||||
super(scheduler);
|
||||
_channel = channel;
|
||||
_selector = selector;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getLocalAddress()
|
||||
{
|
||||
return (InetSocketAddress)_channel.socket().getLocalSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen()
|
||||
{
|
||||
return _channel.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doShutdownOutput()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doClose()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("doClose {}", this);
|
||||
try
|
||||
{
|
||||
_channel.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
LOG.debug("Unable to close channel", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
super.doClose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(Throwable cause)
|
||||
{
|
||||
try
|
||||
{
|
||||
super.onClose(cause);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_selector != null)
|
||||
_selector.destroyEndPoint(this, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fill(ByteBuffer buffer) throws IOException
|
||||
{
|
||||
if (isInputShutdown())
|
||||
return -1;
|
||||
|
||||
int pos = BufferUtil.flipToFill(buffer);
|
||||
InetSocketAddress peer = (InetSocketAddress)_channel.receive(buffer);
|
||||
if (peer == null)
|
||||
{
|
||||
BufferUtil.flipToFlush(buffer, pos);
|
||||
return 0;
|
||||
}
|
||||
INET_ADDRESS_ARGUMENT.push(peer);
|
||||
|
||||
notIdle();
|
||||
BufferUtil.flipToFlush(buffer, pos);
|
||||
int filled = buffer.remaining();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("filled {} {}", filled, BufferUtil.toDetailString(buffer));
|
||||
return filled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean flush(ByteBuffer... buffers) throws IOException
|
||||
{
|
||||
boolean flushedAll = true;
|
||||
long flushed = 0;
|
||||
try
|
||||
{
|
||||
InetSocketAddress peer = INET_ADDRESS_ARGUMENT.pop();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("flushing {} buffer(s) to {}", buffers.length - 1, peer);
|
||||
for (ByteBuffer buffer : buffers)
|
||||
{
|
||||
int sent = _channel.send(buffer, peer);
|
||||
if (sent == 0)
|
||||
{
|
||||
flushedAll = false;
|
||||
break;
|
||||
}
|
||||
flushed += sent;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("flushed {} byte(s), all flushed? {} - {}", flushed, flushedAll, this);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new EofException(e);
|
||||
}
|
||||
|
||||
if (flushed > 0)
|
||||
notIdle();
|
||||
|
||||
return flushedAll;
|
||||
}
|
||||
|
||||
public DatagramChannel getChannel()
|
||||
{
|
||||
return _channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTransport()
|
||||
{
|
||||
return _channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void needsFillInterest()
|
||||
{
|
||||
changeInterests(SelectionKey.OP_READ);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onIncompleteFlush()
|
||||
{
|
||||
changeInterests(SelectionKey.OP_WRITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable onSelected()
|
||||
{
|
||||
// This method runs from the selector thread,
|
||||
// possibly concurrently with changeInterests(int).
|
||||
|
||||
int readyOps = _key.readyOps();
|
||||
int oldInterestOps;
|
||||
int newInterestOps;
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
_updatePending = true;
|
||||
// Remove the readyOps, that here can only be OP_READ or OP_WRITE (or both).
|
||||
oldInterestOps = _desiredInterestOps;
|
||||
newInterestOps = oldInterestOps & ~readyOps;
|
||||
_desiredInterestOps = newInterestOps;
|
||||
}
|
||||
|
||||
boolean fillable = (readyOps & SelectionKey.OP_READ) != 0;
|
||||
boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0;
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("onSelected {}->{} r={} w={} for {}", oldInterestOps, newInterestOps, fillable, flushable, this);
|
||||
|
||||
// return task to complete the job
|
||||
Runnable task = fillable
|
||||
? (flushable
|
||||
? _runCompleteWriteFillable
|
||||
: _runFillable)
|
||||
: (flushable
|
||||
? _runCompleteWrite
|
||||
: null);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("task {}", task);
|
||||
return task;
|
||||
}
|
||||
|
||||
private void updateKeyAction(Selector selector)
|
||||
{
|
||||
updateKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateKey()
|
||||
{
|
||||
// This method runs from the selector thread,
|
||||
// possibly concurrently with changeInterests(int).
|
||||
|
||||
try
|
||||
{
|
||||
int oldInterestOps;
|
||||
int newInterestOps;
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
_updatePending = false;
|
||||
oldInterestOps = _currentInterestOps;
|
||||
newInterestOps = _desiredInterestOps;
|
||||
if (oldInterestOps != newInterestOps)
|
||||
{
|
||||
_currentInterestOps = newInterestOps;
|
||||
_key.interestOps(newInterestOps);
|
||||
}
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Key interests updated {} -> {} on {}", oldInterestOps, newInterestOps, this);
|
||||
}
|
||||
catch (CancelledKeyException x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Ignoring key update for cancelled key {}", this, x);
|
||||
close();
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
LOG.warn("Ignoring key update for {}", this, x);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceKey(SelectionKey newKey)
|
||||
{
|
||||
_key = newKey;
|
||||
}
|
||||
|
||||
private void changeInterests(int operation)
|
||||
{
|
||||
// This method runs from any thread, possibly
|
||||
// concurrently with updateKey() and onSelected().
|
||||
|
||||
int oldInterestOps;
|
||||
int newInterestOps;
|
||||
boolean pending;
|
||||
try (AutoLock l = _lock.lock())
|
||||
{
|
||||
pending = _updatePending;
|
||||
oldInterestOps = _desiredInterestOps;
|
||||
newInterestOps = oldInterestOps | operation;
|
||||
if (newInterestOps != oldInterestOps)
|
||||
_desiredInterestOps = newInterestOps;
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("changeInterests p={} {}->{} for {}", pending, oldInterestOps, newInterestOps, this);
|
||||
|
||||
if (!pending && _selector != null)
|
||||
_selector.submit(_updateKeyAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toEndPointString()
|
||||
{
|
||||
// We do a best effort to print the right toString() and that's it.
|
||||
return String.format("%s{io=%d/%d,kio=%d,kro=%d}",
|
||||
super.toEndPointString(),
|
||||
_currentInterestOps,
|
||||
_desiredInterestOps,
|
||||
ManagedSelector.safeInterestOps(_key),
|
||||
ManagedSelector.safeReadyOps(_key));
|
||||
}
|
||||
|
||||
public final static class InetAddressArgument
|
||||
{
|
||||
private final ThreadLocal<InetSocketAddress> threadLocal = new ThreadLocal<>();
|
||||
|
||||
public void push(InetSocketAddress inetSocketAddress)
|
||||
{
|
||||
Objects.requireNonNull(inetSocketAddress);
|
||||
threadLocal.set(inetSocketAddress);
|
||||
}
|
||||
|
||||
public InetSocketAddress pop()
|
||||
{
|
||||
InetSocketAddress inetSocketAddress = threadLocal.get();
|
||||
Objects.requireNonNull(inetSocketAddress);
|
||||
threadLocal.remove();
|
||||
return inetSocketAddress;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.client.AbstractHttpClientTransport;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpDestination;
|
||||
import org.eclipse.jetty.client.HttpRequest;
|
||||
import org.eclipse.jetty.client.MultiplexConnectionPool;
|
||||
import org.eclipse.jetty.client.MultiplexHttpDestination;
|
||||
import org.eclipse.jetty.client.Origin;
|
||||
import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
|
||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.annotation.ManagedObject;
|
||||
|
||||
@ManagedObject("The QUIC client transport")
|
||||
public class HttpClientTransportOverQuic extends AbstractHttpClientTransport
|
||||
{
|
||||
private final ClientConnectionFactory connectionFactory = new HttpClientConnectionFactory();
|
||||
private final QuicClientConnector connector;
|
||||
private final Origin.Protocol protocol;
|
||||
|
||||
public HttpClientTransportOverQuic()
|
||||
{
|
||||
this("http/1.1");
|
||||
}
|
||||
|
||||
public HttpClientTransportOverQuic(String... alpnProtocols)
|
||||
{
|
||||
//TODO the Protocol instance should be passed around instead of the alpn string array
|
||||
connector = new QuicClientConnector(alpnProtocols);
|
||||
protocol = new Origin.Protocol(Arrays.asList(alpnProtocols), false);
|
||||
addBean(connector);
|
||||
setConnectionPoolFactory(destination ->
|
||||
{
|
||||
HttpClient httpClient = getHttpClient();
|
||||
int maxConnections = httpClient.getMaxConnectionsPerDestination();
|
||||
return new MultiplexConnectionPool(destination, maxConnections, destination, httpClient.getMaxRequestsQueuedPerDestination());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
|
||||
{
|
||||
endPoint.setIdleTimeout(getHttpClient().getIdleTimeout());
|
||||
return connectionFactory.newConnection(endPoint, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Origin newOrigin(HttpRequest request)
|
||||
{
|
||||
return getHttpClient().createOrigin(request, protocol);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpDestination newHttpDestination(Origin origin)
|
||||
{
|
||||
return new MultiplexHttpDestination(getHttpClient(), origin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(InetSocketAddress address, Map<String, Object> context)
|
||||
{
|
||||
HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY);
|
||||
context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, destination.getClientConnectionFactory());
|
||||
@SuppressWarnings("unchecked")
|
||||
Promise<org.eclipse.jetty.client.api.Connection> promise = (Promise<org.eclipse.jetty.client.api.Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, promise::failed));
|
||||
connector.connect(address, context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,366 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.StandardSocketOptions;
|
||||
import java.nio.channels.SelectableChannel;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.eclipse.jetty.http3.quiche.QuicheConfig;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.IClientConnector;
|
||||
import org.eclipse.jetty.io.ManagedSelector;
|
||||
import org.eclipse.jetty.io.MappedByteBufferPool;
|
||||
import org.eclipse.jetty.io.SelectorManager;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QuicClientConnector extends ContainerLifeCycle implements IClientConnector
|
||||
{
|
||||
public static final String CLIENT_CONNECTOR_CONTEXT_KEY = "org.eclipse.jetty.client.connector";
|
||||
public static final String REMOTE_SOCKET_ADDRESS_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".remoteSocketAddress";
|
||||
public static final String CLIENT_CONNECTION_FACTORY_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".clientConnectionFactory";
|
||||
public static final String CONNECTION_PROMISE_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".connectionPromise";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientConnector.class);
|
||||
|
||||
private final QuicheConfig quicheConfig;
|
||||
private Executor executor;
|
||||
private Scheduler scheduler;
|
||||
private ByteBufferPool byteBufferPool;
|
||||
private SslContextFactory.Client sslContextFactory;
|
||||
private ClientDatagramSelectorManager selectorManager;
|
||||
private int selectors = 1;
|
||||
private Duration connectTimeout = Duration.ofSeconds(5);
|
||||
private Duration idleTimeout = Duration.ofSeconds(30);
|
||||
private SocketAddress bindAddress;
|
||||
private boolean reuseAddress = true;
|
||||
|
||||
public QuicClientConnector(String... alpnProtocol)
|
||||
{
|
||||
quicheConfig = new QuicheConfig();
|
||||
quicheConfig.setApplicationProtos(alpnProtocol);
|
||||
quicheConfig.setMaxIdleTimeout(5000L);
|
||||
quicheConfig.setInitialMaxData(10000000L);
|
||||
quicheConfig.setInitialMaxStreamDataBidiLocal(10000000L);
|
||||
quicheConfig.setInitialMaxStreamDataUni(10000000L);
|
||||
quicheConfig.setInitialMaxStreamsBidi(100L);
|
||||
quicheConfig.setInitialMaxStreamsUni(100L);
|
||||
quicheConfig.setDisableActiveMigration(true);
|
||||
quicheConfig.setVerifyPeer(false);
|
||||
}
|
||||
|
||||
public Executor getExecutor()
|
||||
{
|
||||
return executor;
|
||||
}
|
||||
|
||||
public void setExecutor(Executor executor)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
updateBean(this.executor, executor);
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public Scheduler getScheduler()
|
||||
{
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
public void setScheduler(Scheduler scheduler)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
updateBean(this.scheduler, scheduler);
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
public ByteBufferPool getByteBufferPool()
|
||||
{
|
||||
return byteBufferPool;
|
||||
}
|
||||
|
||||
public void setByteBufferPool(ByteBufferPool byteBufferPool)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
updateBean(this.byteBufferPool, byteBufferPool);
|
||||
this.byteBufferPool = byteBufferPool;
|
||||
}
|
||||
|
||||
public SslContextFactory.Client getSslContextFactory()
|
||||
{
|
||||
return sslContextFactory;
|
||||
}
|
||||
|
||||
public void setSslContextFactory(SslContextFactory.Client sslContextFactory)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
updateBean(this.sslContextFactory, sslContextFactory);
|
||||
this.sslContextFactory = sslContextFactory;
|
||||
}
|
||||
|
||||
public int getSelectors()
|
||||
{
|
||||
return selectors;
|
||||
}
|
||||
|
||||
public void setSelectors(int selectors)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException();
|
||||
this.selectors = selectors;
|
||||
}
|
||||
|
||||
public boolean isConnectBlocking()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setConnectBlocking(boolean connectBlocking)
|
||||
{
|
||||
}
|
||||
|
||||
public Duration getConnectTimeout()
|
||||
{
|
||||
return connectTimeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(Duration connectTimeout)
|
||||
{
|
||||
this.connectTimeout = connectTimeout;
|
||||
if (selectorManager != null)
|
||||
selectorManager.setConnectTimeout(connectTimeout.toMillis());
|
||||
}
|
||||
|
||||
public Duration getIdleTimeout()
|
||||
{
|
||||
return idleTimeout;
|
||||
}
|
||||
|
||||
public void setIdleTimeout(Duration idleTimeout)
|
||||
{
|
||||
this.idleTimeout = idleTimeout;
|
||||
}
|
||||
|
||||
public SocketAddress getBindAddress()
|
||||
{
|
||||
return bindAddress;
|
||||
}
|
||||
|
||||
public void setBindAddress(SocketAddress bindAddress)
|
||||
{
|
||||
this.bindAddress = bindAddress;
|
||||
}
|
||||
|
||||
public boolean getReuseAddress()
|
||||
{
|
||||
return reuseAddress;
|
||||
}
|
||||
|
||||
public void setReuseAddress(boolean reuseAddress)
|
||||
{
|
||||
this.reuseAddress = reuseAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception
|
||||
{
|
||||
if (executor == null)
|
||||
{
|
||||
QueuedThreadPool clientThreads = new QueuedThreadPool();
|
||||
clientThreads.setName(String.format("client-pool@%x", hashCode()));
|
||||
setExecutor(clientThreads);
|
||||
}
|
||||
if (scheduler == null)
|
||||
setScheduler(new ScheduledExecutorScheduler(String.format("client-scheduler@%x", hashCode()), false));
|
||||
if (byteBufferPool == null)
|
||||
setByteBufferPool(new MappedByteBufferPool());
|
||||
if (sslContextFactory == null)
|
||||
setSslContextFactory(newSslContextFactory());
|
||||
selectorManager = newSelectorManager();
|
||||
selectorManager.setConnectTimeout(getConnectTimeout().toMillis());
|
||||
addBean(selectorManager);
|
||||
super.doStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStop() throws Exception
|
||||
{
|
||||
super.doStop();
|
||||
removeBean(selectorManager);
|
||||
}
|
||||
|
||||
protected SslContextFactory.Client newSslContextFactory()
|
||||
{
|
||||
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(false);
|
||||
sslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
|
||||
return sslContextFactory;
|
||||
}
|
||||
|
||||
protected ClientDatagramSelectorManager newSelectorManager()
|
||||
{
|
||||
return new ClientDatagramSelectorManager(getExecutor(), getScheduler(), getSelectors());
|
||||
}
|
||||
|
||||
public void connect(SocketAddress address, Map<String, Object> context)
|
||||
{
|
||||
DatagramChannel channel = null;
|
||||
try
|
||||
{
|
||||
if (context == null)
|
||||
context = new HashMap<>();
|
||||
context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this);
|
||||
context.putIfAbsent(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
|
||||
|
||||
channel = DatagramChannel.open();
|
||||
SocketAddress bindAddress = getBindAddress();
|
||||
if (bindAddress != null)
|
||||
{
|
||||
boolean reuseAddress = getReuseAddress();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Binding to {} to connect to {}{}", bindAddress, address, (reuseAddress ? " reusing address" : ""));
|
||||
channel.setOption(StandardSocketOptions.SO_REUSEADDR, reuseAddress);
|
||||
channel.bind(bindAddress);
|
||||
}
|
||||
configure(channel);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Connecting to {}", address);
|
||||
channel.configureBlocking(false);
|
||||
|
||||
selectorManager.connect(channel, context);
|
||||
}
|
||||
// Must catch all exceptions, since some like
|
||||
// UnresolvedAddressException are not IOExceptions.
|
||||
catch (Throwable x)
|
||||
{
|
||||
// If IPv6 is not deployed, a generic SocketException "Network is unreachable"
|
||||
// exception is being thrown, so we attempt to provide a better error message.
|
||||
if (x.getClass() == SocketException.class)
|
||||
x = new SocketException("Could not connect to " + address).initCause(x);
|
||||
IO.close(channel);
|
||||
connectFailed(x, context);
|
||||
}
|
||||
}
|
||||
|
||||
public void accept(DatagramChannel channel, Map<String, Object> context)
|
||||
{
|
||||
try
|
||||
{
|
||||
context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this);
|
||||
if (!channel.isConnected())
|
||||
throw new IllegalStateException("DatagramChannel must be connected");
|
||||
configure(channel);
|
||||
channel.configureBlocking(false);
|
||||
selectorManager.accept(channel, context);
|
||||
}
|
||||
catch (Throwable failure)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Could not accept {}", channel);
|
||||
IO.close(channel);
|
||||
Promise<?> promise = (Promise<?>)context.get(CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
if (promise != null)
|
||||
promise.failed(failure);
|
||||
}
|
||||
}
|
||||
|
||||
protected void configure(DatagramChannel channel) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
protected EndPoint newEndPoint(DatagramChannel channel, ManagedSelector selector, SelectionKey selectionKey)
|
||||
{
|
||||
return new ClientDatagramEndPoint(channel, selector, selectionKey, getScheduler());
|
||||
}
|
||||
|
||||
protected void connectFailed(Throwable failure, Map<String, Object> context)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Could not connect to {}", context.get(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY));
|
||||
Promise<?> promise = (Promise<?>)context.get(CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
if (promise != null)
|
||||
promise.failed(failure);
|
||||
}
|
||||
|
||||
protected class ClientDatagramSelectorManager extends SelectorManager
|
||||
{
|
||||
public ClientDatagramSelectorManager(Executor executor, Scheduler scheduler, int selectors)
|
||||
{
|
||||
super(executor, scheduler, selectors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(SelectableChannel channel, Object attachment)
|
||||
{
|
||||
throw new UnsupportedOperationException("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey)
|
||||
{
|
||||
EndPoint endPoint = QuicClientConnector.this.newEndPoint((DatagramChannel)channel, selector, selectionKey);
|
||||
endPoint.setIdleTimeout(getIdleTimeout().toMillis());
|
||||
return endPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection newConnection(SelectableChannel channel, EndPoint endPoint, Object attachment) throws IOException
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> context = (Map<String, Object>)attachment;
|
||||
return new QuicConnection(executor, scheduler, byteBufferPool, endPoint, context, quicheConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionOpened(Connection connection, Object context)
|
||||
{
|
||||
super.connectionOpened(connection, context);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> contextMap = (Map<String, Object>)context;
|
||||
@SuppressWarnings("unchecked")
|
||||
Promise<Connection> promise = (Promise<Connection>)contextMap.get(CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
if (promise != null)
|
||||
promise.succeeded(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void connectionFailed(SelectableChannel channel, Throwable failure, Object attachment)
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> context = (Map<String, Object>)attachment;
|
||||
connectFailed(failure, context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.client;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.eclipse.jetty.http3.quiche.QuicheConfig;
|
||||
import org.eclipse.jetty.http3.quiche.QuicheConnection;
|
||||
import org.eclipse.jetty.http3.quiche.QuicheConnectionId;
|
||||
import org.eclipse.jetty.http3.quiche.ffi.LibQuiche;
|
||||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IteratingCallback;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QuicConnection extends AbstractConnection
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QuicConnection.class);
|
||||
|
||||
private final ConcurrentMap<QuicheConnectionId, QuicSession> sessions = new ConcurrentHashMap<>();
|
||||
private final QuicheConfig quicheConfig;
|
||||
private final ByteBufferPool byteBufferPool;
|
||||
private final Flusher flusher = new Flusher();
|
||||
private final Scheduler scheduler;
|
||||
private final Map<String, Object> context;
|
||||
|
||||
public QuicConnection(Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, EndPoint endPoint, Map<String, Object> context, QuicheConfig quicheConfig)
|
||||
{
|
||||
super(endPoint, executor);
|
||||
this.quicheConfig = quicheConfig;
|
||||
this.scheduler = scheduler;
|
||||
this.byteBufferPool = byteBufferPool;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
void onClose(QuicheConnectionId quicheConnectionId)
|
||||
{
|
||||
sessions.remove(quicheConnectionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
sessions.values().forEach(QuicSession::close);
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen()
|
||||
{
|
||||
super.onOpen();
|
||||
fillInterested();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFillable()
|
||||
{
|
||||
try
|
||||
{
|
||||
ByteBuffer cipherBuffer = byteBufferPool.acquire(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN, true);
|
||||
while (true)
|
||||
{
|
||||
BufferUtil.clear(cipherBuffer);
|
||||
int fill = getEndPoint().fill(cipherBuffer);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("filled cipher buffer with {} byte(s)", fill);
|
||||
// ServerDatagramEndPoint will only return -1 if input is shut down.
|
||||
if (fill < 0)
|
||||
{
|
||||
byteBufferPool.release(cipherBuffer);
|
||||
getEndPoint().shutdownOutput();
|
||||
return;
|
||||
}
|
||||
if (fill == 0)
|
||||
{
|
||||
byteBufferPool.release(cipherBuffer);
|
||||
fillInterested();
|
||||
return;
|
||||
}
|
||||
|
||||
InetSocketAddress remoteAddress = ClientDatagramEndPoint.INET_ADDRESS_ARGUMENT.pop();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("decoded peer IP address: {}, ciphertext packet size: {}", remoteAddress, cipherBuffer.remaining());
|
||||
|
||||
QuicheConnectionId quicheConnectionId = QuicheConnectionId.fromPacket(cipherBuffer);
|
||||
if (quicheConnectionId == null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("packet contains undecipherable connection ID, dropping it");
|
||||
continue;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("packet contains connection ID {}", quicheConnectionId);
|
||||
|
||||
QuicSession session = sessions.get(quicheConnectionId);
|
||||
if (session == null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("no existing session with connection ID {}, trying to accept new QUIC connection", quicheConnectionId);
|
||||
QuicheConnection quicheConnection = QuicheConnection.tryAccept(quicheConfig, remoteAddress, cipherBuffer);
|
||||
if (quicheConnection == null)
|
||||
{
|
||||
ByteBuffer negotiationBuffer = byteBufferPool.acquire(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN, true);
|
||||
int pos = BufferUtil.flipToFill(negotiationBuffer);
|
||||
if (!QuicheConnection.negotiate(remoteAddress, cipherBuffer, negotiationBuffer))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("QUIC connection negotiation failed, dropping packet");
|
||||
byteBufferPool.release(negotiationBuffer);
|
||||
continue;
|
||||
}
|
||||
BufferUtil.flipToFlush(negotiationBuffer, pos);
|
||||
|
||||
ClientDatagramEndPoint.INET_ADDRESS_ARGUMENT.push(remoteAddress);
|
||||
getEndPoint().write(Callback.from(() -> byteBufferPool.release(negotiationBuffer)), negotiationBuffer);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("QUIC connection negotiation packet sent");
|
||||
}
|
||||
else
|
||||
{
|
||||
session = new QuicSession(getExecutor(), scheduler, byteBufferPool, context, quicheConnectionId, quicheConnection, this, remoteAddress);
|
||||
sessions.putIfAbsent(quicheConnectionId, session);
|
||||
session.flush(); // send the response packet(s) that accept generated.
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("created QUIC session {} with connection ID {}", session, quicheConnectionId);
|
||||
}
|
||||
|
||||
// Once here, cipherBuffer has been fully consumed.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("packet is for existing session with connection ID {}, processing it ({} byte(s))", quicheConnectionId, cipherBuffer.remaining());
|
||||
session.process(remoteAddress, cipherBuffer);
|
||||
}
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("caught exception in onFillable loop", x);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public void write(Callback callback, InetSocketAddress remoteAddress, ByteBuffer... buffers)
|
||||
{
|
||||
flusher.offer(callback, remoteAddress, buffers);
|
||||
flusher.iterate();
|
||||
}
|
||||
|
||||
private class Flusher extends IteratingCallback
|
||||
{
|
||||
private final AutoLock lock = new AutoLock();
|
||||
private final ArrayDeque<Entry> queue = new ArrayDeque<>();
|
||||
private Entry entry;
|
||||
|
||||
public void offer(Callback callback, InetSocketAddress address, ByteBuffer[] buffers)
|
||||
{
|
||||
try (AutoLock l = lock.lock())
|
||||
{
|
||||
queue.offer(new Entry(callback, address, buffers));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Action process()
|
||||
{
|
||||
try (AutoLock l = lock.lock())
|
||||
{
|
||||
entry = queue.poll();
|
||||
}
|
||||
if (entry == null)
|
||||
return Action.IDLE;
|
||||
|
||||
ClientDatagramEndPoint.INET_ADDRESS_ARGUMENT.push(entry.address);
|
||||
getEndPoint().write(this, entry.buffers);
|
||||
return Action.SCHEDULED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
entry.callback.succeeded();
|
||||
super.succeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
entry.callback.failed(x);
|
||||
super.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteFailure(Throwable cause)
|
||||
{
|
||||
QuicConnection.this.close();
|
||||
}
|
||||
|
||||
private class Entry
|
||||
{
|
||||
private final Callback callback;
|
||||
private final InetSocketAddress address;
|
||||
private final ByteBuffer[] buffers;
|
||||
|
||||
private Entry(Callback callback, InetSocketAddress address, ByteBuffer[] buffers)
|
||||
{
|
||||
this.callback = callback;
|
||||
this.address = address;
|
||||
this.buffers = buffers;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.http3.quiche.QuicheConnection;
|
||||
import org.eclipse.jetty.http3.quiche.QuicheConnectionId;
|
||||
import org.eclipse.jetty.http3.quiche.ffi.LibQuiche;
|
||||
import org.eclipse.jetty.io.AbstractEndPoint;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.CyclicTimeout;
|
||||
import org.eclipse.jetty.io.RuntimeIOException;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.IteratingCallback;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.util.thread.ExecutionStrategy;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QuicSession
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QuicSession.class);
|
||||
|
||||
private final Flusher flusher;
|
||||
private final Scheduler scheduler;
|
||||
private final ByteBufferPool byteBufferPool;
|
||||
private final Map<String, Object> context;
|
||||
private final QuicheConnectionId quicheConnectionId;
|
||||
private final QuicheConnection quicheConnection;
|
||||
private final QuicConnection connection;
|
||||
private final ConcurrentMap<Long, QuicStreamEndPoint> endpoints = new ConcurrentHashMap<>();
|
||||
private final ExecutionStrategy strategy;
|
||||
private final AutoLock strategyQueueLock = new AutoLock();
|
||||
private final Queue<Runnable> strategyQueue = new ArrayDeque<>();
|
||||
private InetSocketAddress remoteAddress;
|
||||
|
||||
QuicSession(Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, Map<String, Object> context, QuicheConnectionId quicheConnectionId, QuicheConnection quicheConnection, QuicConnection connection, InetSocketAddress remoteAddress)
|
||||
{
|
||||
this.scheduler = scheduler;
|
||||
this.byteBufferPool = byteBufferPool;
|
||||
this.context = context;
|
||||
this.quicheConnectionId = quicheConnectionId;
|
||||
this.quicheConnection = quicheConnection;
|
||||
this.connection = connection;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.flusher = new Flusher(scheduler);
|
||||
this.strategy = new EatWhatYouKill(() ->
|
||||
{
|
||||
try (AutoLock l = strategyQueueLock.lock())
|
||||
{
|
||||
return strategyQueue.poll();
|
||||
}
|
||||
}, executor);
|
||||
LifeCycle.start(strategy);
|
||||
}
|
||||
|
||||
public int fill(long streamId, ByteBuffer buffer) throws IOException
|
||||
{
|
||||
return quicheConnection.drainClearTextForStream(streamId, buffer);
|
||||
}
|
||||
|
||||
public int flush(long streamId, ByteBuffer buffer) throws IOException
|
||||
{
|
||||
int flushed = quicheConnection.feedClearTextForStream(streamId, buffer);
|
||||
flush();
|
||||
return flushed;
|
||||
}
|
||||
|
||||
public void flushFinished(long streamId) throws IOException
|
||||
{
|
||||
quicheConnection.feedFinForStream(streamId);
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isFinished(long streamId)
|
||||
{
|
||||
return quicheConnection.isStreamFinished(streamId);
|
||||
}
|
||||
|
||||
public void shutdownInput(long streamId) throws IOException
|
||||
{
|
||||
quicheConnection.shutdownStream(streamId, false);
|
||||
}
|
||||
|
||||
public void shutdownOutput(long streamId) throws IOException
|
||||
{
|
||||
quicheConnection.shutdownStream(streamId, true);
|
||||
}
|
||||
|
||||
public void onClose(long streamId)
|
||||
{
|
||||
endpoints.remove(streamId);
|
||||
}
|
||||
|
||||
InetSocketAddress getLocalAddress()
|
||||
{
|
||||
return connection.getEndPoint().getLocalAddress();
|
||||
}
|
||||
|
||||
InetSocketAddress getRemoteAddress()
|
||||
{
|
||||
return remoteAddress;
|
||||
}
|
||||
|
||||
void process(InetSocketAddress remoteAddress, ByteBuffer cipherBufferIn) throws IOException
|
||||
{
|
||||
this.remoteAddress = remoteAddress;
|
||||
quicheConnection.feedCipherText(cipherBufferIn);
|
||||
|
||||
if (quicheConnection.isConnectionEstablished())
|
||||
{
|
||||
List<Long> writableStreamIds = quicheConnection.writableStreamIds();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("writable stream ids: {}", writableStreamIds);
|
||||
Runnable onWritable = () ->
|
||||
{
|
||||
for (Long writableStreamId : writableStreamIds)
|
||||
{
|
||||
onWritable(writableStreamId);
|
||||
}
|
||||
};
|
||||
dispatch(onWritable);
|
||||
|
||||
List<Long> readableStreamIds = quicheConnection.readableStreamIds();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("readable stream ids: {}", readableStreamIds);
|
||||
for (Long readableStreamId : readableStreamIds)
|
||||
{
|
||||
Runnable onReadable = () -> onReadable(readableStreamId);
|
||||
dispatch(onReadable);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void onWritable(long writableStreamId)
|
||||
{
|
||||
QuicStreamEndPoint streamEndPoint = getOrCreateStreamEndPoint(writableStreamId);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("selected endpoint for write: {}", streamEndPoint);
|
||||
streamEndPoint.onWritable();
|
||||
}
|
||||
|
||||
private void onReadable(long readableStreamId)
|
||||
{
|
||||
QuicStreamEndPoint streamEndPoint = getOrCreateStreamEndPoint(readableStreamId);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("selected endpoint for read: {}", streamEndPoint);
|
||||
streamEndPoint.onReadable();
|
||||
}
|
||||
|
||||
private void dispatch(Runnable runnable)
|
||||
{
|
||||
try (AutoLock l = strategyQueueLock.lock())
|
||||
{
|
||||
strategyQueue.offer(runnable);
|
||||
}
|
||||
strategy.dispatch();
|
||||
}
|
||||
|
||||
void flush()
|
||||
{
|
||||
flusher.iterate();
|
||||
}
|
||||
|
||||
private QuicStreamEndPoint getOrCreateStreamEndPoint(long streamId)
|
||||
{
|
||||
QuicStreamEndPoint endPoint = endpoints.compute(streamId, (sid, quicStreamEndPoint) ->
|
||||
{
|
||||
if (quicStreamEndPoint == null)
|
||||
{
|
||||
quicStreamEndPoint = createQuicStreamEndPoint(streamId);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("creating endpoint for stream {}", sid);
|
||||
}
|
||||
return quicStreamEndPoint;
|
||||
});
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("returning endpoint for stream {}", streamId);
|
||||
return endPoint;
|
||||
}
|
||||
|
||||
private QuicStreamEndPoint createQuicStreamEndPoint(long streamId)
|
||||
{
|
||||
ClientConnectionFactory connectionFactory = (ClientConnectionFactory)context.get(QuicClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY);
|
||||
QuicStreamEndPoint endPoint = new QuicStreamEndPoint(scheduler, this, streamId);
|
||||
try
|
||||
{
|
||||
Connection connection = connectionFactory.newConnection(endPoint, context);
|
||||
endPoint.setConnection(connection);
|
||||
endPoint.onOpen();
|
||||
connection.onOpen();
|
||||
return endPoint;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeIOException("Error creating new connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void close()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("closing QUIC session {}", this);
|
||||
try
|
||||
{
|
||||
endpoints.values().forEach(AbstractEndPoint::close);
|
||||
endpoints.clear();
|
||||
flusher.close();
|
||||
connection.onClose(quicheConnectionId);
|
||||
LifeCycle.stop(strategy);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// This call frees malloc'ed memory so make sure it always happens.
|
||||
quicheConnection.dispose();
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("closed QUIC session {}", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getClass().getSimpleName() + " id=" + quicheConnectionId;
|
||||
}
|
||||
|
||||
private class Flusher extends IteratingCallback
|
||||
{
|
||||
private final CyclicTimeout timeout;
|
||||
private ByteBuffer cipherBuffer;
|
||||
|
||||
public Flusher(Scheduler scheduler)
|
||||
{
|
||||
timeout = new CyclicTimeout(scheduler) {
|
||||
@Override
|
||||
public void onTimeoutExpired()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("quiche timeout callback");
|
||||
quicheConnection.onTimeout();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("re-iterating quiche after timeout");
|
||||
iterate();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
super.close();
|
||||
timeout.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Action process() throws IOException
|
||||
{
|
||||
cipherBuffer = byteBufferPool.acquire(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN, true);
|
||||
int pos = BufferUtil.flipToFill(cipherBuffer);
|
||||
int drained = quicheConnection.drainCipherText(cipherBuffer);
|
||||
long nextTimeoutInMs = quicheConnection.nextTimeout();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("next quiche timeout: {} ms", nextTimeoutInMs);
|
||||
if (nextTimeoutInMs < 0)
|
||||
timeout.cancel();
|
||||
else
|
||||
timeout.schedule(nextTimeoutInMs, TimeUnit.MILLISECONDS);
|
||||
if (drained == 0)
|
||||
{
|
||||
if (quicheConnection.isConnectionClosed())
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("quiche connection is in closed state");
|
||||
QuicSession.this.close();
|
||||
}
|
||||
return Action.IDLE;
|
||||
}
|
||||
BufferUtil.flipToFlush(cipherBuffer, pos);
|
||||
connection.write(this, remoteAddress, cipherBuffer);
|
||||
return Action.SCHEDULED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
byteBufferPool.release(cipherBuffer);
|
||||
super.succeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteFailure(Throwable cause)
|
||||
{
|
||||
byteBufferPool.release(cipherBuffer);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.io.AbstractEndPoint;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QuicStreamEndPoint extends AbstractEndPoint
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QuicStreamEndPoint.class);
|
||||
|
||||
private final QuicSession session;
|
||||
private final long streamId;
|
||||
|
||||
public QuicStreamEndPoint(Scheduler scheduler, QuicSession session, long streamId)
|
||||
{
|
||||
super(scheduler);
|
||||
this.session = session;
|
||||
this.streamId = streamId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getLocalAddress()
|
||||
{
|
||||
return session.getLocalAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress()
|
||||
{
|
||||
return session.getRemoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doShutdownInput()
|
||||
{
|
||||
try
|
||||
{
|
||||
session.shutdownInput(streamId);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("error shutting down output", x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doShutdownOutput()
|
||||
{
|
||||
try
|
||||
{
|
||||
session.shutdownOutput(streamId);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("error shutting down output", x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(Throwable failure)
|
||||
{
|
||||
try
|
||||
{
|
||||
session.flushFinished(streamId);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Error sending FIN on stream {}", streamId, e);
|
||||
}
|
||||
super.onClose(failure);
|
||||
session.onClose(streamId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fill(ByteBuffer buffer) throws IOException
|
||||
{
|
||||
int pos = BufferUtil.flipToFill(buffer);
|
||||
int drained = session.fill(streamId, buffer);
|
||||
BufferUtil.flipToFlush(buffer, pos);
|
||||
if (session.isFinished(streamId))
|
||||
shutdownInput();
|
||||
return drained;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean flush(ByteBuffer... buffers) throws IOException
|
||||
{
|
||||
for (ByteBuffer buffer : buffers)
|
||||
{
|
||||
int flushed = session.flush(streamId, buffer);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("flushed {} bytes to stream {}; buffer has remaining? {}", flushed, streamId, buffer.hasRemaining());
|
||||
if (buffer.hasRemaining())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTransport()
|
||||
{
|
||||
return session;
|
||||
}
|
||||
|
||||
public void onWritable()
|
||||
{
|
||||
getWriteFlusher().completeWrite();
|
||||
}
|
||||
|
||||
public void onReadable()
|
||||
{
|
||||
getFillInterest().fillable();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onIncompleteFlush()
|
||||
{
|
||||
// No need to do anything.
|
||||
// See QuicSession.process().
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void needsFillInterest()
|
||||
{
|
||||
// No need to do anything.
|
||||
// See QuicSession.process().
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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.http3.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpCompliance;
|
||||
import org.eclipse.jetty.http3.server.ServerDatagramConnector;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
|
||||
public class End2EndClientTest
|
||||
{
|
||||
private Server server;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
|
||||
HttpConfiguration config = new HttpConfiguration();
|
||||
config.setHttpCompliance(HttpCompliance.LEGACY); // enable HTTP/0.9
|
||||
HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config);
|
||||
|
||||
ServerDatagramConnector serverDatagramConnector = new ServerDatagramConnector(server, connectionFactory);
|
||||
serverDatagramConnector.setPort(8443);
|
||||
server.addConnector(serverDatagramConnector);
|
||||
|
||||
server.setHandler(new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.println("<html>\n" +
|
||||
"\t<body>\n" +
|
||||
"\t\tRequest served\n" +
|
||||
"\t</body>\n" +
|
||||
"</html>");
|
||||
}
|
||||
});
|
||||
|
||||
server.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() throws Exception
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void name() throws Exception
|
||||
{
|
||||
HttpClientTransportOverQuic transport = new HttpClientTransportOverQuic();
|
||||
HttpClient client = new HttpClient(transport);
|
||||
client.start();
|
||||
|
||||
ContentResponse response = client.GET("https://localhost:8443/");
|
||||
int status = response.getStatus();
|
||||
String contentAsString = response.getContentAsString();
|
||||
System.out.println("==========");
|
||||
System.out.println("Status: " + status);
|
||||
System.out.println(contentAsString);
|
||||
System.out.println("==========");
|
||||
|
||||
client.stop();
|
||||
|
||||
assertThat(status, is(200));
|
||||
assertThat(contentAsString, is("<html>\n" +
|
||||
"\t<body>\n" +
|
||||
"\t\tRequest served\n" +
|
||||
"\t</body>\n" +
|
||||
"</html>\n"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Jetty Logging using jetty-slf4j-impl
|
||||
#org.eclipse.jetty.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.http3.LEVEL=DEBUG
|
Binary file not shown.
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.http3</groupId>
|
||||
<artifactId>http3-parent</artifactId>
|
||||
<version>10.0.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>http3-common</artifactId>
|
||||
<name>Jetty :: HTTP3 :: Common</name>
|
||||
|
||||
<properties>
|
||||
<bundle-symbolic-name>${project.groupId}.common</bundle-symbolic-name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http3</groupId>
|
||||
<artifactId>cloudflare-quiche-jna</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-io</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-slf4j-impl</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 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
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
module org.eclipse.jetty.http3.common
|
||||
{
|
||||
|
||||
requires transitive org.eclipse.jetty.http3.quiche;
|
||||
requires transitive org.eclipse.jetty.io;
|
||||
requires org.slf4j;
|
||||
}
|
|
@ -26,7 +26,7 @@
|
|||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http3</groupId>
|
||||
<artifactId>cloudflare-quiche-jna</artifactId>
|
||||
<artifactId>http3-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
|
||||
<modules>
|
||||
<module>cloudflare-quiche-jna</module>
|
||||
<module>http3-common</module>
|
||||
<module>http3-server</module>
|
||||
<module>http3-client</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
|
Loading…
Reference in New Issue