Issue #6728 - QUIC and HTTP/3

- Continued implementation of HTTP/3 APIs, frames and parsing, client and server.
- Figured out creation of streams, to support special HTTP/3 streams.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2021-09-11 18:04:56 +02:00
parent 3cbbb47742
commit 7c09800fe1
41 changed files with 1235 additions and 288 deletions

View File

@ -0,0 +1,36 @@
<?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.7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>http3-client</artifactId>
<name>Jetty :: HTTP3 :: Client</name>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.http3</groupId>
<artifactId>http3-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-client</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-util</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,23 @@
//
// ========================================================================
// 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.client
{
exports org.eclipse.jetty.http3.client;
requires transitive org.eclipse.jetty.http3.common;
requires transitive org.eclipse.jetty.io;
requires transitive org.eclipse.jetty.quic.common;
requires org.eclipse.jetty.quic.client;
requires transitive org.eclipse.jetty.util;
}

View File

@ -0,0 +1,71 @@
//
// ========================================================================
// 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.SocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jetty.http3.api.Session;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.quic.client.ClientQuicConnection;
import org.eclipse.jetty.quic.client.QuicClientConnectorConfigurator;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
public class HTTP3Client extends ContainerLifeCycle
{
public static final String CLIENT_CONTEXT_KEY = HTTP3Client.class.getName();
private static final String SESSION_LISTENER_CONTEXT_KEY = CLIENT_CONTEXT_KEY + ".listener";
private static final String SESSION_PROMISE_CONTEXT_KEY = CLIENT_CONTEXT_KEY + ".promise";
private final ClientConnector connector;
private List<String> protocols = List.of("h3");
public HTTP3Client()
{
this.connector = new ClientConnector(new QuicClientConnectorConfigurator());
addBean(connector);
}
@ManagedAttribute("The ALPN protocol list")
public List<String> getProtocols()
{
return protocols;
}
public void setProtocols(List<String> protocols)
{
this.protocols = protocols;
}
public CompletableFuture<Session> connect(SocketAddress address, Session.Listener listener)
{
Promise.Completable<Session> completable = new Promise.Completable<>();
ClientConnectionFactory factory = new HTTP3ClientConnectionFactory();
Map<String, Object> context = new ConcurrentHashMap<>();
context.put(CLIENT_CONTEXT_KEY, this);
context.put(SESSION_LISTENER_CONTEXT_KEY, listener);
context.put(SESSION_PROMISE_CONTEXT_KEY, completable);
context.put(ClientQuicConnection.APPLICATION_PROTOCOLS, getProtocols());
context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, factory);
context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, completable::failed));
connector.connect(address, context);
return completable;
}
}

View File

@ -0,0 +1,41 @@
//
// ========================================================================
// 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.util.Map;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.quic.client.ClientQuicSession;
import org.eclipse.jetty.quic.common.ProtocolQuicSession;
import org.eclipse.jetty.quic.common.QuicSession;
public class HTTP3ClientConnectionFactory implements ClientConnectionFactory, ProtocolQuicSession.Factory
{
@Override
public ProtocolQuicSession newProtocolQuicSession(QuicSession quicSession, Map<String, Object> context)
{
HTTP3Client http3Client = (HTTP3Client)context.get(HTTP3Client.CLIENT_CONTEXT_KEY);
// TODO: configure the QpackDecoder.maxHeaderSize from HTTP3Client
return new HTTP3ClientQuicSession((ClientQuicSession)quicSession);
}
@Override
public Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
{
return null;
}
}

View File

@ -0,0 +1,79 @@
//
// ========================================================================
// 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 org.eclipse.jetty.http3.internal.ControlConnection;
import org.eclipse.jetty.http3.internal.DecoderConnection;
import org.eclipse.jetty.http3.internal.EncoderConnection;
import org.eclipse.jetty.quic.client.ClientQuicSession;
import org.eclipse.jetty.quic.client.ProtocolClientQuicSession;
import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
public class HTTP3ClientQuicSession extends ProtocolClientQuicSession
{
private QuicStreamEndPoint decoderEndPoint;
private QuicStreamEndPoint encoderEndPoint;
private QuicStreamEndPoint controlEndPoint;
public HTTP3ClientQuicSession(ClientQuicSession session)
{
super(session);
}
@Override
public void onOpen()
{
long decoderStreamId = getQuicSession().newClientUnidirectionalStreamId();
decoderEndPoint = configureDecoderEndPoint(decoderStreamId);
long encoderStreamId = getQuicSession().newClientUnidirectionalStreamId();
encoderEndPoint = configureEncoderEndPoint(encoderStreamId);
long controlStreamId = getQuicSession().newClientBidirectionalStreamId();
controlEndPoint = configureControlEndPoint(controlStreamId);
}
private QuicStreamEndPoint configureDecoderEndPoint(long streamId)
{
return getOrCreateStreamEndPoint(streamId, endPoint ->
{
DecoderConnection connection = new DecoderConnection(endPoint, getQuicSession().getExecutor());
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
});
}
private QuicStreamEndPoint configureEncoderEndPoint(long streamId)
{
return getOrCreateStreamEndPoint(streamId, endPoint ->
{
EncoderConnection connection = new EncoderConnection(endPoint, getQuicSession().getExecutor());
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
});
}
private QuicStreamEndPoint configureControlEndPoint(long streamId)
{
return getOrCreateStreamEndPoint(streamId, endPoint ->
{
ControlConnection connection = new ControlConnection(endPoint, getQuicSession().getExecutor());
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
});
}
}

View File

@ -1,8 +1,8 @@
<?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>
<artifactId>http3-parent</artifactId>
<groupId>org.eclipse.jetty.http3</groupId>
<artifactId>http3-parent</artifactId>
<version>10.0.7-SNAPSHOT</version>
</parent>

View File

@ -0,0 +1,30 @@
//
// ========================================================================
// 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
{
exports org.eclipse.jetty.http3;
exports org.eclipse.jetty.http3.api;
exports org.eclipse.jetty.http3.api.server;
exports org.eclipse.jetty.http3.frames;
exports org.eclipse.jetty.http3.internal.parser to org.eclipse.jetty.http3.server;
exports org.eclipse.jetty.http3.internal;
requires transitive org.eclipse.jetty.http;
requires org.eclipse.jetty.http3.qpack;
requires org.eclipse.jetty.io;
requires org.eclipse.jetty.quic.common;
requires org.eclipse.jetty.util;
requires org.slf4j;
}

View File

@ -13,6 +13,13 @@
package org.eclipse.jetty.http3.api;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.http3.frames.HeadersFrame;
public interface Session
{
public CompletableFuture<Stream> newStream(HeadersFrame frame, Stream.Listener listener);
public interface Listener {}
}

View File

@ -15,4 +15,5 @@ package org.eclipse.jetty.http3.api;
public interface Stream
{
public interface Listener {}
}

View File

@ -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
// ========================================================================
//
package org.eclipse.jetty.http3.api.server;
import org.eclipse.jetty.http3.api.Session;
public interface ServerSessionListener extends Session.Listener
{
}

View File

@ -0,0 +1,33 @@
//
// ========================================================================
// 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.internal;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.EndPoint;
public class ControlConnection extends AbstractConnection
{
public ControlConnection(EndPoint endPoint, Executor executor)
{
super(endPoint, executor);
}
@Override
public void onFillable()
{
}
}

View File

@ -0,0 +1,33 @@
//
// ========================================================================
// 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.internal;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.EndPoint;
public class DecoderConnection extends AbstractConnection
{
public DecoderConnection(EndPoint endPoint, Executor executor)
{
super(endPoint, executor);
}
@Override
public void onFillable()
{
}
}

View File

@ -0,0 +1,33 @@
//
// ========================================================================
// 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.internal;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.EndPoint;
public class EncoderConnection extends AbstractConnection
{
public EncoderConnection(EndPoint endPoint, Executor executor)
{
super(endPoint, executor);
}
@Override
public void onFillable()
{
}
}

View File

@ -13,9 +13,18 @@
package org.eclipse.jetty.http3.internal;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.http3.api.Session;
import org.eclipse.jetty.http3.api.Stream;
import org.eclipse.jetty.http3.frames.HeadersFrame;
import org.eclipse.jetty.http3.internal.parser.Parser;
public class HTTP3Session implements Session, Parser.Listener
{
@Override
public CompletableFuture<Stream> newStream(HeadersFrame frame, Stream.Listener listener)
{
return null;
}
}

View File

@ -1,8 +1,8 @@
<?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>
<artifactId>http3-parent</artifactId>
<groupId>org.eclipse.jetty.http3</groupId>
<artifactId>http3-parent</artifactId>
<version>10.0.7-SNAPSHOT</version>
</parent>
@ -23,7 +23,7 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-common</artifactId>
<artifactId>quic-server</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

View File

@ -0,0 +1,23 @@
//
// ========================================================================
// 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.server
{
exports org.eclipse.jetty.http3.server;
requires transitive org.eclipse.jetty.http3.common;
requires transitive org.eclipse.jetty.http3.qpack;
requires transitive org.eclipse.jetty.io;
requires transitive org.eclipse.jetty.quic.server;
requires transitive org.eclipse.jetty.server;
}

View File

@ -0,0 +1,68 @@
//
// ========================================================================
// 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.server;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.http3.internal.HTTP3Connection;
import org.eclipse.jetty.http3.internal.HTTP3Session;
import org.eclipse.jetty.http3.internal.parser.Parser;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.quic.common.ProtocolQuicSession;
import org.eclipse.jetty.quic.common.QuicSession;
import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
import org.eclipse.jetty.quic.server.ServerQuicSession;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
public abstract class AbstractHTTP3ServerConnectionFactory extends AbstractConnectionFactory implements ProtocolQuicSession.Factory
{
private final HttpConfiguration httpConfiguration;
public AbstractHTTP3ServerConnectionFactory(HttpConfiguration httpConfiguration)
{
super("h3");
this.httpConfiguration = Objects.requireNonNull(httpConfiguration);
addBean(httpConfiguration);
}
public HttpConfiguration getHttpConfiguration()
{
return httpConfiguration;
}
@Override
public ProtocolQuicSession newProtocolQuicSession(QuicSession quicSession, Map<String, Object> context)
{
return new HTTP3ServerQuicSession((ServerQuicSession)quicSession, getHttpConfiguration().getResponseHeaderSize());
}
@Override
public Connection newConnection(Connector connector, EndPoint endPoint)
{
// TODO: can the downcasts be removed?
long streamId = ((QuicStreamEndPoint)endPoint).getStreamId();
HTTP3ServerQuicSession http3QuicSession = (HTTP3ServerQuicSession)((QuicStreamEndPoint)endPoint).getQuicSession().getProtocolQuicSession();
HTTP3Session session = new HTTP3Session();
Parser parser = new Parser(streamId, session, http3QuicSession.getQpackDecoder());
HTTP3Connection connection = new HTTP3Connection(endPoint, connector.getExecutor(), parser);
return connection;
}
}

View File

@ -1,49 +0,0 @@
//
// ========================================================================
// 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.server;
import java.util.List;
import org.eclipse.jetty.http3.qpack.Instruction;
import org.eclipse.jetty.http3.qpack.QpackDecoder;
import org.eclipse.jetty.http3.qpack.QpackException;
import org.eclipse.jetty.quic.common.ProtocolQuicSession;
import org.eclipse.jetty.quic.common.QuicSession;
public class HTTP3QuicSession extends ProtocolQuicSession
{
private final QpackDecoder decoder;
private final Instruction.Handler decoderHandler = new QpackDecoderInstructionHandler();
public HTTP3QuicSession(QuicSession session, int maxHeaderSize)
{
super(session);
decoder = new QpackDecoder(decoderHandler, maxHeaderSize);
// TODO: create a streamId for the Instruction stream.
}
public QpackDecoder getQpackDecoder()
{
return decoder;
}
private class QpackDecoderInstructionHandler implements Instruction.Handler
{
@Override
public void onInstructions(List<Instruction> instructions) throws QpackException
{
// TODO: feed the Instruction to QuicSession.
}
}
}

View File

@ -13,24 +13,10 @@
package org.eclipse.jetty.http3.server;
import java.util.Objects;
import org.eclipse.jetty.http3.internal.HTTP3Connection;
import org.eclipse.jetty.http3.internal.HTTP3Session;
import org.eclipse.jetty.http3.internal.parser.Parser;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.quic.common.ProtocolQuicSession;
import org.eclipse.jetty.quic.common.QuicSession;
import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
public class HTTP3ServerConnectionFactory extends AbstractConnectionFactory implements ProtocolQuicSession.Factory
public class HTTP3ServerConnectionFactory extends AbstractHTTP3ServerConnectionFactory
{
private final HttpConfiguration httpConfiguration;
public HTTP3ServerConnectionFactory()
{
this(new HttpConfiguration());
@ -38,29 +24,6 @@ public class HTTP3ServerConnectionFactory extends AbstractConnectionFactory impl
public HTTP3ServerConnectionFactory(HttpConfiguration configuration)
{
super("h3");
this.httpConfiguration = Objects.requireNonNull(configuration);
addBean(httpConfiguration);
}
@Override
public ProtocolQuicSession newProtocolQuicSession(QuicSession quicSession)
{
return new HTTP3QuicSession(quicSession, httpConfiguration.getResponseHeaderSize());
}
@Override
public Connection newConnection(Connector connector, EndPoint endPoint)
{
// TODO: can the downcasts be removed?
long streamId = ((QuicStreamEndPoint)endPoint).getStreamId();
HTTP3QuicSession http3QuicSession = (HTTP3QuicSession)((QuicStreamEndPoint)endPoint).getQuicSession().getProtocolQuicSession();
HTTP3Session session = new HTTP3Session();
Parser parser = new Parser(streamId, session, http3QuicSession.getQpackDecoder());
HTTP3Connection connection = new HTTP3Connection(endPoint, connector.getExecutor(), parser);
return connection;
super(configuration);
}
}

View File

@ -0,0 +1,142 @@
//
// ========================================================================
// 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.server;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import org.eclipse.jetty.http3.internal.ControlConnection;
import org.eclipse.jetty.http3.internal.DecoderConnection;
import org.eclipse.jetty.http3.internal.EncoderConnection;
import org.eclipse.jetty.http3.qpack.Instruction;
import org.eclipse.jetty.http3.qpack.QpackDecoder;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
import org.eclipse.jetty.quic.server.ProtocolServerQuicSession;
import org.eclipse.jetty.quic.server.ServerQuicSession;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.thread.AutoLock;
public class HTTP3ServerQuicSession extends ProtocolServerQuicSession
{
private final QpackDecoder decoder;
private QuicStreamEndPoint decoderEndPoint;
private QuicStreamEndPoint encoderEndPoint;
private QuicStreamEndPoint controlEndPoint;
public HTTP3ServerQuicSession(ServerQuicSession session, int maxHeaderSize)
{
super(session);
decoder = new QpackDecoder(new QpackDecoderInstructionHandler(), maxHeaderSize);
}
@Override
public void onOpen()
{
long decoderStreamId = getQuicSession().newServerUnidirectionalStreamId();
decoderEndPoint = configureDecoderEndPoint(decoderStreamId);
long encoderStreamId = getQuicSession().newServerUnidirectionalStreamId();
encoderEndPoint = configureEncoderEndPoint(encoderStreamId);
long controlStreamId = getQuicSession().newServerBidirectionalStreamId();
controlEndPoint = configureControlEndPoint(controlStreamId);
}
private QuicStreamEndPoint configureDecoderEndPoint(long streamId)
{
return getOrCreateStreamEndPoint(streamId, endPoint ->
{
DecoderConnection connection = new DecoderConnection(endPoint, getQuicSession().getExecutor());
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
});
}
private QuicStreamEndPoint configureEncoderEndPoint(long streamId)
{
return getOrCreateStreamEndPoint(streamId, endPoint ->
{
EncoderConnection connection = new EncoderConnection(endPoint, getQuicSession().getExecutor());
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
});
}
private QuicStreamEndPoint configureControlEndPoint(long streamId)
{
return getOrCreateStreamEndPoint(streamId, endPoint ->
{
ControlConnection connection = new ControlConnection(endPoint, getQuicSession().getExecutor());
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
});
}
public QpackDecoder getQpackDecoder()
{
return decoder;
}
private class QpackDecoderInstructionHandler extends IteratingCallback implements Instruction.Handler
{
private final AutoLock lock = new AutoLock();
private final ByteBufferPool.Lease lease = new ByteBufferPool.Lease(getQuicSession().getByteBufferPool());
private final Queue<Instruction> queue = new ArrayDeque<>();
@Override
public void onInstructions(List<Instruction> instructions)
{
try (AutoLock l = lock.lock())
{
queue.addAll(instructions);
}
iterate();
}
@Override
protected Action process()
{
List<Instruction> instructions;
try (AutoLock l = lock.lock())
{
if (queue.isEmpty())
return Action.IDLE;
instructions = new ArrayList<>(queue);
}
instructions.forEach(i -> i.encode(lease));
decoderEndPoint.write(this, getQuicSession().getRemoteAddress(), lease.getByteBuffers().toArray(ByteBuffer[]::new));
return Action.SCHEDULED;
}
@Override
public void succeeded()
{
lease.recycle();
super.succeeded();
}
@Override
public InvocationType getInvocationType()
{
return InvocationType.NON_BLOCKING;
}
}
}

View File

@ -0,0 +1,33 @@
//
// ========================================================================
// 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.server;
import org.eclipse.jetty.http3.api.server.ServerSessionListener;
import org.eclipse.jetty.server.HttpConfiguration;
public class RawHTTP3ServerConnectionFactory extends AbstractHTTP3ServerConnectionFactory
{
private final ServerSessionListener listener;
public RawHTTP3ServerConnectionFactory(ServerSessionListener listener)
{
this(new HttpConfiguration(), listener);
}
public RawHTTP3ServerConnectionFactory(HttpConfiguration httpConfiguration, ServerSessionListener listener)
{
super(httpConfiguration);
this.listener = listener;
}
}

View File

@ -0,0 +1,42 @@
<?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.7-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>http3-tests</artifactId>
<name>Jetty :: HTTP3 :: Tests</name>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.http3</groupId>
<artifactId>http3-client</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</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.quic</groupId>
<artifactId>quic-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,68 @@
//
// ========================================================================
// 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.tests;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http3.api.Session;
import org.eclipse.jetty.http3.api.Stream;
import org.eclipse.jetty.http3.api.server.ServerSessionListener;
import org.eclipse.jetty.http3.client.HTTP3Client;
import org.eclipse.jetty.http3.frames.HeadersFrame;
import org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory;
import org.eclipse.jetty.quic.server.ServerQuicConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.Test;
public class HTTP3ClientServerTest
{
@Test
public void testGETThenResponseWithoutContent() throws Exception
{
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
Server server = new Server(serverThreads);
ServerQuicConnector connector = new ServerQuicConnector(server, sslContextFactory, new RawHTTP3ServerConnectionFactory(new ServerSessionListener() {}));
server.addConnector(connector);
server.start();
HTTP3Client client = new HTTP3Client();
client.start();
Session session = client.connect(new InetSocketAddress("localhost", connector.getLocalPort()), new Session.Listener() {})
.get(555, TimeUnit.SECONDS);
System.err.println("session = " + session);
HttpURI uri = HttpURI.from("https://localhost:" + connector.getLocalPort());
MetaData.Request metaData = new MetaData.Request(HttpMethod.GET.asString(), uri, HttpVersion.HTTP_3, HttpFields.EMPTY);
HeadersFrame frame = new HeadersFrame(metaData);
Stream stream = session.newStream(frame, new Stream.Listener() {})
.get(5, TimeUnit.SECONDS);
System.err.println("stream = " + stream);
}
}

View File

@ -0,0 +1,4 @@
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.http3.LEVEL=DEBUG
#org.eclipse.jetty.quic.LEVEL=DEBUG
org.eclipse.jetty.quic.quiche.LEVEL=DEBUG

View File

@ -16,6 +16,8 @@
<module>http3-qpack</module>
<module>http3-common</module>
<module>http3-server</module>
<module>http3-client</module>
<module>http3-tests</module>
</modules>
</project>

View File

@ -47,6 +47,7 @@ import org.slf4j.LoggerFactory;
*/
public class ClientQuicConnection extends QuicConnection
{
public static final String APPLICATION_PROTOCOLS = "org.eclipse.jetty.quic.application.protocols";
private static final Logger LOG = LoggerFactory.getLogger(ClientQuicConnection.class);
private final Map<SocketAddress, QuicSession> pendingSessions = new ConcurrentHashMap<>();
@ -61,39 +62,63 @@ public class ClientQuicConnection extends QuicConnection
@Override
public void onOpen()
{
super.onOpen();
try
{
InetSocketAddress remoteAddress = (InetSocketAddress)context.get(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY);
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
super.onOpen();
@SuppressWarnings("unchecked")
List<String> protocols = (List<String>)context.get(APPLICATION_PROTOCOLS);
if (protocols == null)
{
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
if (destination != null)
protocols = destination.getOrigin().getProtocol().getProtocols();
if (protocols == null)
throw new IllegalStateException("Missing ALPN protocols");
}
// TODO: pull the config settings from somewhere else TBD (context?)
QuicheConfig quicheConfig = new QuicheConfig();
List<String> protocols = destination.getOrigin().getProtocol().getProtocols();
quicheConfig.setApplicationProtos(protocols.toArray(new String[0]));
quicheConfig.setApplicationProtos(protocols.toArray(String[]::new));
quicheConfig.setDisableActiveMigration(true);
quicheConfig.setVerifyPeer(false);
quicheConfig.setMaxIdleTimeout(5000L);
quicheConfig.setMaxIdleTimeout(getEndPoint().getIdleTimeout());
quicheConfig.setInitialMaxData(10_000_000L);
quicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
quicheConfig.setInitialMaxStreamDataUni(10_000_000L);
quicheConfig.setInitialMaxStreamsBidi(100L);
quicheConfig.setInitialMaxStreamsUni(100L);
InetSocketAddress remoteAddress = (InetSocketAddress)context.get(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY);
QuicheConnection quicheConnection = QuicheConnection.connect(quicheConfig, remoteAddress);
QuicSession session = new ClientQuicSession(getExecutor(), getScheduler(), getByteBufferPool(), quicheConnection, this, remoteAddress, context);
pendingSessions.put(remoteAddress, session);
session.flush(); // send the response packet(s) that connect generated.
if (LOG.isDebugEnabled())
LOG.debug("created connecting QUIC session {}", session);
}
catch (IOException e)
{
throw new RuntimeIOException("Error trying to open connection", e);
}
fillInterested();
fillInterested();
}
catch (IOException x)
{
throw new RuntimeIOException(x);
}
}
@Override
protected QuicSession createSession(SocketAddress remoteAddress, ByteBuffer cipherBuffer) throws IOException
{
QuicSession session = pendingSessions.get(remoteAddress);
if (session != null)
{
session.process(remoteAddress, cipherBuffer);
if (session.isConnectionEstablished())
{
pendingSessions.remove(remoteAddress);
return session;
}
}
return null;
}
@Override
@ -109,21 +134,4 @@ public class ClientQuicConnection extends QuicConnection
promise.failed(x);
}
}
@Override
protected QuicSession createSession(SocketAddress remoteAddress, ByteBuffer cipherBuffer) throws IOException
{
QuicSession session = pendingSessions.get(remoteAddress);
if (session != null)
{
session.process(remoteAddress, cipherBuffer);
if (session.isConnectionEstablished())
{
pendingSessions.remove(remoteAddress);
session.onOpen();
return session;
}
}
return null;
}
}

View File

@ -22,7 +22,6 @@ import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.quic.common.ProtocolQuicSession;
import org.eclipse.jetty.quic.common.QuicConnection;
import org.eclipse.jetty.quic.common.QuicSession;
@ -49,25 +48,16 @@ public class ClientQuicSession extends QuicSession
@Override
protected ProtocolQuicSession createProtocolQuicSession()
{
return new ProtocolQuicSession(this);
ClientConnectionFactory connectionFactory = (ClientConnectionFactory)context.get(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY);
if (connectionFactory instanceof ProtocolQuicSession.Factory)
return ((ProtocolQuicSession.Factory)connectionFactory).newProtocolQuicSession(this, context);
return new ProtocolClientQuicSession(this);
}
@Override
protected QuicStreamEndPoint createQuicStreamEndPoint(long streamId)
public Connection newConnection(QuicStreamEndPoint endPoint) throws IOException
{
try
{
ClientConnectionFactory connectionFactory = (ClientConnectionFactory)context.get(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY);
QuicStreamEndPoint endPoint = new QuicStreamEndPoint(getScheduler(), this, streamId);
Connection connection = connectionFactory.newConnection(endPoint, context);
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
return endPoint;
}
catch (IOException x)
{
throw new RuntimeIOException("Error creating new connection", x);
}
ClientConnectionFactory connectionFactory = (ClientConnectionFactory)context.get(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY);
return connectionFactory.newConnection(endPoint, context);
}
}

View File

@ -0,0 +1,82 @@
//
// ========================================================================
// 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.quic.client;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.quic.common.ProtocolQuicSession;
import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProtocolClientQuicSession extends ProtocolQuicSession
{
private static final Logger LOG = LoggerFactory.getLogger(ProtocolClientQuicSession.class);
public ProtocolClientQuicSession(ClientQuicSession session)
{
super(session);
}
@Override
public ClientQuicSession getQuicSession()
{
return (ClientQuicSession)super.getQuicSession();
}
@Override
public void onOpen()
{
// Create a single bidirectional, client-initiated,
// QUIC stream that plays the role of the TCP stream.
configureEndPoint(getQuicSession().newClientBidirectionalStreamId());
process();
}
private void configureEndPoint(long streamId)
{
getOrCreateStreamEndPoint(streamId, endPoint ->
{
try
{
Connection connection = getQuicSession().newConnection(endPoint);
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
}
catch (RuntimeException | Error x)
{
if (LOG.isDebugEnabled())
LOG.debug("could not open protocol QUIC session", x);
throw x;
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("could not open protocol QUIC session", x);
throw new RuntimeException(x);
}
});
}
@Override
protected void onReadable(long readableStreamId)
{
// On the client, we need a get-only semantic in case of reads.
QuicStreamEndPoint streamEndPoint = getStreamEndPoint(readableStreamId);
if (LOG.isDebugEnabled())
LOG.debug("stream {} selected endpoint for read: {}", readableStreamId, streamEndPoint);
if (streamEndPoint != null)
streamEndPoint.onReadable();
}
}

View File

@ -36,6 +36,7 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
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;
@ -58,12 +59,16 @@ public class End2EndClientTest
@BeforeEach
public void setUp() throws Exception
{
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
server = new Server();
HttpConfiguration httpConfiguration = new HttpConfiguration();
HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration);
HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfiguration);
connector = new ServerQuicConnector(server, http1, http2);
connector = new ServerQuicConnector(server, sslContextFactory, http1, http2);
server.addConnector(connector);
server.setHandler(new AbstractHandler()

View File

@ -1,3 +1,3 @@
#org.eclipse.jetty.LEVEL=DEBUG
org.eclipse.jetty.quic.LEVEL=DEBUG
org.eclipse.jetty.quic.quiche.LEVEL=DEBUG
#org.eclipse.jetty.quic.LEVEL=DEBUG
org.eclipse.jetty.quic.quiche.LEVEL=INFO

View File

@ -13,10 +13,18 @@
package org.eclipse.jetty.quic.common;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
public class ProtocolQuicSession
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class ProtocolQuicSession
{
private static final Logger LOG = LoggerFactory.getLogger(ProtocolQuicSession.class);
private final AtomicLong active = new AtomicLong();
private final QuicSession session;
@ -30,6 +38,8 @@ public class ProtocolQuicSession
return session;
}
public abstract void onOpen();
public void process()
{
if (active.getAndIncrement() == 0)
@ -38,8 +48,8 @@ public class ProtocolQuicSession
{
while (true)
{
session.processWritableStreams();
if (session.processReadableStreams())
processWritableStreams();
if (processReadableStreams())
continue;
// Exit if did not process any stream and we are idle.
if (active.decrementAndGet() == 0)
@ -49,8 +59,47 @@ public class ProtocolQuicSession
}
}
protected QuicStreamEndPoint getStreamEndPoint(long streamId)
{
return session.getStreamEndPoint(streamId);
}
protected QuicStreamEndPoint getOrCreateStreamEndPoint(long streamId, Consumer<QuicStreamEndPoint> consumer)
{
return session.getOrCreateStreamEndPoint(streamId, consumer);
}
private void processWritableStreams()
{
List<Long> writableStreamIds = session.getWritableStreamIds();
if (LOG.isDebugEnabled())
LOG.debug("writable stream ids: {}", writableStreamIds);
writableStreamIds.forEach(this::onWritable);
}
protected void onWritable(long writableStreamId)
{
// For both client and server, we only need a get-only semantic in case of writes.
QuicStreamEndPoint streamEndPoint = session.getStreamEndPoint(writableStreamId);
if (LOG.isDebugEnabled())
LOG.debug("stream {} selected endpoint for write: {}", writableStreamId, streamEndPoint);
if (streamEndPoint != null)
streamEndPoint.onWritable();
}
private boolean processReadableStreams()
{
List<Long> readableStreamIds = session.getReadableStreamIds();
if (LOG.isDebugEnabled())
LOG.debug("readable stream ids: {}", readableStreamIds);
readableStreamIds.forEach(this::onReadable);
return !readableStreamIds.isEmpty();
}
protected abstract void onReadable(long readableStreamId);
public interface Factory
{
public ProtocolQuicSession newProtocolQuicSession(QuicSession quicSession);
public ProtocolQuicSession newProtocolQuicSession(QuicSession quicSession, Map<String, Object> context);
}
}

View File

@ -23,8 +23,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.CyclicTimeout;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
@ -54,6 +57,7 @@ public abstract class QuicSession
{
private static final Logger LOG = LoggerFactory.getLogger(QuicSession.class);
private final AtomicLong ids = new AtomicLong();
private final AutoLock strategyQueueLock = new AutoLock();
private final Queue<Runnable> strategyQueue = new ArrayDeque<>();
private final ConcurrentMap<Long, QuicStreamEndPoint> endpoints = new ConcurrentHashMap<>();
@ -66,7 +70,6 @@ public abstract class QuicSession
private final ExecutionStrategy strategy;
private SocketAddress remoteAddress;
private ProtocolQuicSession protocolSession;
// TODO make it final?
private QuicheConnectionId quicheConnectionId;
protected QuicSession(Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, QuicheConnection quicheConnection, QuicConnection connection, SocketAddress remoteAddress)
@ -92,6 +95,11 @@ public abstract class QuicSession
return scheduler;
}
public ByteBufferPool getByteBufferPool()
{
return byteBufferPool;
}
public ProtocolQuicSession getProtocolQuicSession()
{
return protocolSession;
@ -102,9 +110,46 @@ public abstract class QuicSession
return quicheConnection.getNegotiatedProtocol();
}
/**
* @return a new unidirectional, client-initiated, stream ID
*/
public long newClientUnidirectionalStreamId()
{
return newStreamId() + 0x02;
}
/**
* @return a new bidirectional, client-initiated, stream ID
*/
public long newClientBidirectionalStreamId()
{
return newStreamId();
}
/**
* @return a new unidirectional, server-initiated, stream ID
*/
public long newServerUnidirectionalStreamId()
{
return newStreamId() + 0x03;
}
/**
* @return a new bidirectional, server-initiated, stream ID
*/
public long newServerBidirectionalStreamId()
{
return newStreamId() + 0x01;
}
private long newStreamId()
{
return ids.getAndIncrement() << 2;
}
public void onOpen()
{
getOrCreateStreamEndPoint(0);
protocolSession.onOpen();
}
public int fill(long streamId, ByteBuffer buffer) throws IOException
@ -182,19 +227,32 @@ public abstract class QuicSession
// client1 -- sockEP1 -> H2Connection - HEADERSParser - H2Session -* RequestStreams -# HTTP Handler
// client2 -- sockEP2 -> H2Connection - HEADERSParser - H2Session -* RequestStreams -# HTTP Handler
// HTTP/1 on QUIC
// client1
// \
// dataEP - QuicConnection -* QuicSession -# ProtocolQuicSession -* RequestStreamN - HttpConnection - HTTP Handler
// /
// client2
// HTTP/3
// client1
// \ /- ConnectionStream0 - ConnectionParser for SETTINGS frames, etc.
// dataEP - QuicConnection -* QuicSession -# ProtocolQuicSession -* RequestStreamsEP - H3Connection - HEADERSParser -# HTTP Handler
// / `- InstructionStream - InstructionConnection/Parser
// \ /- ConnectionStream0 - ConnectionParser for SETTINGS frames, etc.
// dataEP - QuicConnection -* QuicSession -# H3QuicSession -* RequestStreamsEP - H3Connection - HEADERSParser -# HTTP Handler
// / `- InstructionStream - InstructionConnection/Parser
// client2
// H3ProtoSession - QpackEncoder
// H3ProtoSession - QpackDecoder
// H3ProtoSession -* request streams
if (protocolSession == null)
{
protocolSession = createProtocolQuicSession();
protocolSession.process();
onOpen();
}
else
{
protocolSession.process();
}
}
else
{
@ -204,38 +262,22 @@ public abstract class QuicSession
protected abstract ProtocolQuicSession createProtocolQuicSession();
void processWritableStreams()
List<Long> getWritableStreamIds()
{
List<Long> writableStreamIds = quicheConnection.writableStreamIds();
if (LOG.isDebugEnabled())
LOG.debug("writable stream ids: {}", writableStreamIds);
writableStreamIds.forEach(this::onWritable);
return quicheConnection.writableStreamIds();
}
private void onWritable(long writableStreamId)
List<Long> getReadableStreamIds()
{
QuicStreamEndPoint streamEndPoint = getOrCreateStreamEndPoint(writableStreamId);
if (LOG.isDebugEnabled())
LOG.debug("selected endpoint for write: {}", streamEndPoint);
streamEndPoint.onWritable();
return quicheConnection.readableStreamIds();
}
boolean processReadableStreams()
QuicStreamEndPoint getStreamEndPoint(long streamId)
{
List<Long> readableStreamIds = quicheConnection.readableStreamIds();
if (LOG.isDebugEnabled())
LOG.debug("readable stream ids: {}", readableStreamIds);
readableStreamIds.forEach(this::onReadable);
return !readableStreamIds.isEmpty();
return endpoints.get(streamId);
}
private void onReadable(long readableStreamId)
{
QuicStreamEndPoint streamEndPoint = getOrCreateStreamEndPoint(readableStreamId);
if (LOG.isDebugEnabled())
LOG.debug("selected endpoint for read: {}", streamEndPoint);
streamEndPoint.onReadable();
}
public abstract Connection newConnection(QuicStreamEndPoint endPoint) throws IOException;
private void dispatch(Runnable runnable)
{
@ -253,15 +295,16 @@ public abstract class QuicSession
flusher.iterate();
}
private QuicStreamEndPoint getOrCreateStreamEndPoint(long streamId)
public QuicStreamEndPoint getOrCreateStreamEndPoint(long streamId, Consumer<QuicStreamEndPoint> consumer)
{
QuicStreamEndPoint endPoint = endpoints.compute(streamId, (sid, quicStreamEndPoint) ->
{
if (quicStreamEndPoint == null)
{
quicStreamEndPoint = createQuicStreamEndPoint(streamId);
if (LOG.isDebugEnabled())
LOG.debug("creating endpoint for stream {}", sid);
quicStreamEndPoint = newQuicStreamEndPoint(streamId);
consumer.accept(quicStreamEndPoint);
}
return quicStreamEndPoint;
});
@ -270,7 +313,10 @@ public abstract class QuicSession
return endPoint;
}
protected abstract QuicStreamEndPoint createQuicStreamEndPoint(long streamId);
private QuicStreamEndPoint newQuicStreamEndPoint(long streamId)
{
return new QuicStreamEndPoint(getScheduler(), this, streamId);
}
public void close()
{

View File

@ -184,4 +184,10 @@ public class QuicStreamEndPoint extends AbstractEndPoint
// No need to do anything.
// See QuicSession.process().
}
@Override
public String toString()
{
return String.format("%s@%x#%d[%s]->[%s]", getClass().getSimpleName(), hashCode(), getStreamId(), toEndPointString(), toConnectionString());
}
}

View File

@ -44,9 +44,11 @@ public class QuicheConnectionId
this.string = bytesToHex(dcid);
}
private static String bytesToHex(byte[] bytes) {
private static String bytesToHex(byte[] bytes)
{
byte[] hexChars = new byte[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
for (int j = 0; j < bytes.length; j++)
{
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
@ -60,9 +62,9 @@ public class QuicheConnectionId
return new QuicheConnectionId(sizedDcid);
}
static QuicheConnectionId fromCid(byte[] dcid, size_t_pointer dcid_len)
static QuicheConnectionId fromCid(byte[] dcid, size_t_pointer dcidLen)
{
byte[] sizedDcid = resizeIfNeeded(dcid, (int)dcid_len.getValue());
byte[] sizedDcid = resizeIfNeeded(dcid, (int)dcidLen.getValue());
return new QuicheConnectionId(sizedDcid);
}
@ -76,23 +78,23 @@ public class QuicheConnectionId
// Source Connection ID
byte[] scid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer scid_len = new size_t_pointer(scid.length);
size_t_pointer scidLen = new size_t_pointer(scid.length);
// Destination Connection ID
byte[] dcid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer dcid_len = new size_t_pointer(dcid.length);
size_t_pointer dcidLen = new size_t_pointer(dcid.length);
byte[] token = new byte[32];
size_t_pointer token_len = new size_t_pointer(token.length);
size_t_pointer tokenLen = new size_t_pointer(token.length);
int rc = LibQuiche.INSTANCE.quiche_header_info(packet, new size_t(packet.remaining()), new size_t(LibQuiche.QUICHE_MAX_CONN_ID_LEN),
version, type,
scid, scid_len,
dcid, dcid_len,
token, token_len);
scid, scidLen,
dcid, dcidLen,
token, tokenLen);
if (rc < 0)
return null;
return fromCid(dcid, dcid_len);
return fromCid(dcid, dcidLen);
}
private static byte[] resizeIfNeeded(byte[] buffer, int length)

View File

@ -15,7 +15,7 @@ module org.eclipse.jetty.quic.server
{
exports org.eclipse.jetty.quic.server;
requires org.eclipse.jetty.quic.common;
requires transitive org.eclipse.jetty.quic.common;
requires org.eclipse.jetty.quic.quiche;
requires org.eclipse.jetty.io;
requires org.eclipse.jetty.server;

View File

@ -0,0 +1,60 @@
//
// ========================================================================
// 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.quic.server;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.quic.common.ProtocolQuicSession;
import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProtocolServerQuicSession extends ProtocolQuicSession
{
private static final Logger LOG = LoggerFactory.getLogger(ProtocolServerQuicSession.class);
public ProtocolServerQuicSession(ServerQuicSession session)
{
super(session);
}
@Override
public ServerQuicSession getQuicSession()
{
return (ServerQuicSession)super.getQuicSession();
}
@Override
public void onOpen()
{
process();
}
@Override
protected void onReadable(long readableStreamId)
{
// On the server, we need a get-or-create semantic in case of reads.
QuicStreamEndPoint streamEndPoint = getOrCreateStreamEndPoint(readableStreamId, this::configureEndPoint);
if (LOG.isDebugEnabled())
LOG.debug("stream {} selected endpoint for read: {}", readableStreamId, streamEndPoint);
streamEndPoint.onReadable();
}
private void configureEndPoint(QuicStreamEndPoint endPoint)
{
Connection connection = getQuicSession().newConnection(endPoint);
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
}
}

View File

@ -88,7 +88,8 @@ public class ServerQuicConnection extends QuicConnection
else
{
QuicSession session = new ServerQuicSession(getExecutor(), getScheduler(), byteBufferPool, quicheConnection, this, remoteAddress, connector);
session.flush(); // send the response packet(s) that tryAccept() generated.
// Send the response packet(s) that tryAccept() generated.
session.flush();
return session;
}
}

View File

@ -35,64 +35,29 @@ import org.eclipse.jetty.server.AbstractNetworkConnector;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.Scheduler;
// TODO: add configuration and figure out interaction with SslContextFactory.
public class ServerQuicConnector extends AbstractNetworkConnector
{
private final ServerDatagramSelectorManager _manager;
private final QuicheConfig _quicheConfig;
private final SslContextFactory.Server _sslContextFactory;
private final QuicheConfig _quicheConfig = new QuicheConfig();
private volatile DatagramChannel _datagramChannel;
private volatile int _localPort = -1;
public ServerQuicConnector(
@Name("server") Server server,
@Name("executor") Executor executor,
@Name("scheduler") Scheduler scheduler,
@Name("bufferPool") ByteBufferPool bufferPool,
@Name("selectors") int selectors,
@Name("factories") ConnectionFactory... factories)
public ServerQuicConnector(Server server, SslContextFactory.Server sslContextFactory, ConnectionFactory... factories)
{
super(server, executor, scheduler, bufferPool, 0, factories);
_manager = new ServerDatagramSelectorManager(getExecutor(), getScheduler(), selectors);
addBean(_manager, true);
setAcceptorPriorityDelta(-2);
File[] files;
try
{
SSLKeyPair keyPair;
keyPair = new SSLKeyPair(new File("src/test/resources/keystore.p12"), "PKCS12", "storepwd".toCharArray(), "mykey", "storepwd".toCharArray());
files = keyPair.export(new File(System.getProperty("java.io.tmpdir")));
}
catch (Exception e)
{
throw new RuntimeException(e);
}
// TODO make the QuicheConfig configurable
_quicheConfig = new QuicheConfig();
_quicheConfig.setPrivKeyPemPath(files[0].getPath());
_quicheConfig.setCertChainPemPath(files[1].getPath());
_quicheConfig.setVerifyPeer(false);
_quicheConfig.setMaxIdleTimeout(5000L);
_quicheConfig.setInitialMaxData(10000000L);
_quicheConfig.setInitialMaxStreamDataBidiLocal(10000000L);
_quicheConfig.setInitialMaxStreamDataBidiRemote(10000000L);
_quicheConfig.setInitialMaxStreamDataUni(10000000L);
_quicheConfig.setInitialMaxStreamsBidi(100L);
_quicheConfig.setCongestionControl(QuicheConfig.CongestionControl.RENO);
List<String> protocols = getProtocols();
protocols.add(0, "http/0.9"); // TODO this is only needed for Quiche example clients
_quicheConfig.setApplicationProtos(protocols.toArray(new String[0]));
this(server, null, null, null, sslContextFactory, factories);
}
public ServerQuicConnector(
@Name("server") Server server,
@Name("factories") ConnectionFactory... factories)
public ServerQuicConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool bufferPool, SslContextFactory.Server sslContextFactory, ConnectionFactory... factories)
{
this(server, null, null, null, 1, factories);
super(server, executor, scheduler, bufferPool, 0, factories);
_manager = new ServerDatagramSelectorManager(getExecutor(), getScheduler(), 1);
addBean(_manager);
_sslContextFactory = sslContextFactory;
addBean(_sslContextFactory);
}
@Override
@ -101,6 +66,13 @@ public class ServerQuicConnector extends AbstractNetworkConnector
return _localPort;
}
@Override
public boolean isOpen()
{
DatagramChannel channel = _datagramChannel;
return channel != null && channel.isOpen();
}
@Override
protected void doStart() throws Exception
{
@ -108,21 +80,34 @@ public class ServerQuicConnector extends AbstractNetworkConnector
_manager.addEventListener(l);
super.doStart();
_manager.accept(_datagramChannel);
}
@Override
protected void doStop() throws Exception
{
super.doStop();
for (EventListener l : getBeans(EventListener.class))
_manager.removeEventListener(l);
}
String alias = _sslContextFactory.getCertAlias();
char[] keyStorePassword = _sslContextFactory.getKeyStorePassword().toCharArray();
String keyManagerPassword = _sslContextFactory.getKeyManagerPassword();
SSLKeyPair keyPair = new SSLKeyPair(
_sslContextFactory.getKeyStoreResource().getFile(),
_sslContextFactory.getKeyStoreType(),
keyStorePassword,
alias == null ? "mykey" : alias,
keyManagerPassword == null ? keyStorePassword : keyManagerPassword.toCharArray()
);
File[] pemFiles = keyPair.export(new File(System.getProperty("java.io.tmpdir")));
@Override
public boolean isOpen()
{
DatagramChannel channel = _datagramChannel;
return channel != null && channel.isOpen();
// TODO: make the QuicheConfig configurable.
_quicheConfig.setPrivKeyPemPath(pemFiles[0].getPath());
_quicheConfig.setCertChainPemPath(pemFiles[1].getPath());
_quicheConfig.setVerifyPeer(false);
_quicheConfig.setMaxIdleTimeout(getIdleTimeout());
_quicheConfig.setInitialMaxData(10000000L);
_quicheConfig.setInitialMaxStreamDataBidiLocal(10000000L);
_quicheConfig.setInitialMaxStreamDataBidiRemote(10000000L);
_quicheConfig.setInitialMaxStreamDataUni(10000000L);
_quicheConfig.setInitialMaxStreamsBidi(100L);
_quicheConfig.setCongestionControl(QuicheConfig.CongestionControl.RENO);
List<String> protocols = getProtocols();
// This is only needed for Quiche example clients.
protocols.add(0, "http/0.9");
_quicheConfig.setApplicationProtos(protocols.toArray(String[]::new));
}
@Override
@ -134,44 +119,18 @@ public class ServerQuicConnector extends AbstractNetworkConnector
_datagramChannel.configureBlocking(false);
_localPort = _datagramChannel.socket().getLocalPort();
if (_localPort <= 0)
throw new IOException("Datagram channel not bound");
throw new IOException("DatagramChannel not bound");
addBean(_datagramChannel);
}
}
@Override
public void close()
{
super.close();
DatagramChannel datagramChannel = _datagramChannel;
_datagramChannel = null;
if (datagramChannel != null)
{
removeBean(datagramChannel);
if (datagramChannel.isOpen())
{
try
{
datagramChannel.close();
}
catch (IOException e)
{
LOG.warn("Unable to close {}", datagramChannel, e);
}
}
}
_localPort = -2;
}
protected DatagramChannel openDatagramChannel() throws IOException
{
InetSocketAddress bindAddress = getHost() == null ? new InetSocketAddress(getPort()) : new InetSocketAddress(getHost(), getPort());
DatagramChannel datagramChannel = DatagramChannel.open();
try
{
datagramChannel.socket().bind(bindAddress);
datagramChannel.bind(bindAddress);
}
catch (Throwable e)
{
@ -181,6 +140,28 @@ public class ServerQuicConnector extends AbstractNetworkConnector
return datagramChannel;
}
@Override
protected void doStop() throws Exception
{
super.doStop();
for (EventListener l : getBeans(EventListener.class))
_manager.removeEventListener(l);
}
@Override
public void close()
{
super.close();
DatagramChannel datagramChannel = _datagramChannel;
_datagramChannel = null;
if (datagramChannel != null)
{
removeBean(datagramChannel);
IO.close(datagramChannel);
}
_localPort = -2;
}
@Override
public Object getTransport()
{

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.quic.server;
import java.net.SocketAddress;
import java.util.Map;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.ByteBufferPool;
@ -50,20 +51,15 @@ public class ServerQuicSession extends QuicSession
{
ConnectionFactory connectionFactory = findConnectionFactory(getNegotiatedProtocol());
if (connectionFactory instanceof ProtocolQuicSession.Factory)
return ((ProtocolQuicSession.Factory)connectionFactory).newProtocolQuicSession(this);
return new ProtocolQuicSession(this);
return ((ProtocolQuicSession.Factory)connectionFactory).newProtocolQuicSession(this, Map.of());
return new ProtocolServerQuicSession(this);
}
@Override
protected QuicStreamEndPoint createQuicStreamEndPoint(long streamId)
public Connection newConnection(QuicStreamEndPoint endPoint)
{
ConnectionFactory connectionFactory = findConnectionFactory(getNegotiatedProtocol());
QuicStreamEndPoint endPoint = new QuicStreamEndPoint(getScheduler(), this, streamId);
Connection connection = connectionFactory.newConnection(connector, endPoint);
endPoint.setConnection(connection);
endPoint.onOpen();
connection.onOpen();
return endPoint;
return connectionFactory.newConnection(connector, endPoint);
}
private ConnectionFactory findConnectionFactory(String negotiatedProtocol)

View File

@ -25,6 +25,7 @@ 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.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -34,13 +35,17 @@ public class ServerQuicConnectorTest
@Test
public void testSmall() throws Exception
{
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
Server server = new Server();
HttpConfiguration config = new HttpConfiguration();
config.setHttpCompliance(HttpCompliance.LEGACY); // enable HTTP/0.9
HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config);
ServerQuicConnector connector = new ServerQuicConnector(server, connectionFactory);
ServerQuicConnector connector = new ServerQuicConnector(server, sslContextFactory, connectionFactory);
connector.setPort(8443);
server.addConnector(connector);
@ -71,13 +76,17 @@ public class ServerQuicConnectorTest
@Test
public void testBig() throws Exception
{
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
Server server = new Server();
HttpConfiguration config = new HttpConfiguration();
config.setHttpCompliance(HttpCompliance.LEGACY); // enable HTTP/0.9
HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config);
ServerQuicConnector connector = new ServerQuicConnector(server, connectionFactory);
ServerQuicConnector connector = new ServerQuicConnector(server, sslContextFactory, connectionFactory);
connector.setPort(8443);
server.addConnector(connector);