465857 - Support HTTP/2 clear-text server-side upgrade.

Fixed and test both types of upgrade, from HTTP/1.1 via its
upgrade mechanism, and direct HTTP/2.
This commit is contained in:
Simone Bordet 2015-04-29 22:26:17 +02:00
parent aaaf65bf3c
commit a6cc4ff2f5
10 changed files with 269 additions and 143 deletions

View File

@ -66,9 +66,9 @@ public class HTTP2Connection extends AbstractConnection
return parser;
}
protected void prefill(ByteBuffer buffer)
protected void setInputBuffer(ByteBuffer buffer)
{
producer.buffer=buffer;
producer.buffer = buffer;
}
@Override

View File

@ -18,14 +18,28 @@
package org.eclipse.jetty.http2.frames;
import java.nio.charset.StandardCharsets;
public class PrefaceFrame extends Frame
{
public static final byte[] PREFACE_BYTES = new byte[]
{
0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54,
0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a,
0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a
};
/**
* The bytes of the HTTP/2 preface that form a legal HTTP/1.1
* request, used in the direct upgrade.
*/
public static final byte[] PREFACE_PREAMBLE_BYTES = (
"PRI * HTTP/2.0\r\n" +
"\r\n"
).getBytes(StandardCharsets.US_ASCII);
/**
* The HTTP/2 preface bytes.
*/
public static final byte[] PREFACE_BYTES = (
"PRI * HTTP/2.0\r\n" +
"\r\n" +
"SM\r\n" +
"\r\n"
).getBytes(StandardCharsets.US_ASCII);
public PrefaceFrame()
{

View File

@ -38,20 +38,19 @@ public class PrefaceParser
this.listener = listener;
}
/* ------------------------------------------------------------ */
/** Unsafe upgrade is an unofficial upgrade from HTTP/1.0 to HTTP/2.0
* initiated when a the <code>org.eclipse.jetty.server.HttpConnection</code> sees a PRI * HTTP/2.0 prefix
* that indicates a HTTP/2.0 client is attempting a h2c direct connection.
* This is not a standard HTTP/1.1 Upgrade path.
/**
* <p>Advances this parser after the {@link PrefaceFrame#PREFACE_PREAMBLE_BYTES}.</p>
* <p>This allows the HTTP/1.1 parser to parse the preamble of the preface,
* which is a legal HTTP/1.1 request, and this parser will parse the remaining
* bytes, that are not parseable by a HTTP/1.1 parser.</p>
*/
public void directUpgrade()
protected void directUpgrade()
{
if (cursor!=0)
if (cursor != 0)
throw new IllegalStateException();
cursor=18;
cursor = PrefaceFrame.PREFACE_PREAMBLE_BYTES.length;
}
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())

View File

@ -41,20 +41,25 @@ public class ServerParser extends Parser
this.prefaceParser = new PrefaceParser(listener);
}
/* ------------------------------------------------------------ */
/** Unsafe upgrade is an unofficial upgrade from HTTP/1.0 to HTTP/2.0
* initiated when a the <code>org.eclipse.jetty.server.HttpConnection</code> sees a PRI * HTTP/2.0 prefix
* that indicates a HTTP/2.0 client is attempting a h2c direct connection.
* This is not a standard HTTP/1.1 Upgrade path.
/**
* <p>A direct upgrade is an unofficial upgrade from HTTP/1.1 to HTTP/2.0.</p>
* <p>A direct upgrade is initiated when {@code org.eclipse.jetty.server.HttpConnection}
* sees a request with these bytes:</p>
* <pre>
* PRI * HTTP/2.0\r\n
* \r\n
* </pre>
* <p>This request is part of the HTTP/2.0 preface, indicating that a
* HTTP/2.0 client is attempting a h2c direct connection.</p>
* <p>This is not a standard HTTP/1.1 Upgrade path.</p>
*/
public void directUpgrade()
{
if (state!=State.PREFACE)
if (state != State.PREFACE)
throw new IllegalStateException();
prefaceParser.directUpgrade();
}
@Override
public void parse(ByteBuffer buffer)
{

View File

@ -25,7 +25,6 @@ import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.HTTP2Connection;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
@ -43,7 +42,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
public AbstractHTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration)
{
this(httpConfiguration,"h2-17","h2-16","h2-15","h2-14","h2");
this(httpConfiguration,"h2","h2-17","h2-16","h2-15","h2-14");
}
protected AbstractHTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration, String... protocols)
@ -103,7 +102,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
// stream idle timeout will expire earlier that the connection's.
session.setStreamIdleTimeout(endPoint.getIdleTimeout());
Parser parser = newServerParser(connector, session);
ServerParser parser = newServerParser(connector, session);
HTTP2Connection connection = new HTTP2ServerConnection(connector.getByteBufferPool(), connector.getExecutor(),
endPoint, httpConfiguration, parser, session, getInputBufferSize(), listener);

View File

@ -51,7 +51,7 @@ public class HTTP2CServerConnectionFactory extends HTTP2ServerConnectionFactory
public HTTP2CServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration)
{
super(httpConfiguration,"h2c","h2c-14");
super(httpConfiguration,"h2c","h2c-17","h2c-16","h2c-15","h2c-14");
}
@Override
@ -71,7 +71,7 @@ public class HTTP2CServerConnectionFactory extends HTTP2ServerConnectionFactory
return null;
HTTP2ServerConnection connection = (HTTP2ServerConnection)newConnection(connector, endPoint);
if (connection.upgrade(request, response101))
if (connection.upgrade(request))
return connection;
return null;
}

View File

@ -23,7 +23,7 @@ import java.util.Queue;
import java.util.concurrent.Executor;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MetaData;
@ -35,7 +35,6 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.http2.parser.SettingsBodyParser;
import org.eclipse.jetty.io.ByteBufferPool;
@ -56,7 +55,7 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
private final HttpConfiguration httpConfig;
private HeadersFrame upgradeRequest;
public HTTP2ServerConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, HttpConfiguration httpConfig, Parser parser, ISession session, int inputBufferSize, ServerSessionListener listener)
public HTTP2ServerConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, HttpConfiguration httpConfig, ServerParser parser, ISession session, int inputBufferSize, ServerSessionListener listener)
{
super(byteBufferPool, executor, endPoint, parser, session, inputBufferSize);
this.listener = listener;
@ -64,11 +63,17 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
}
@Override
public void onUpgradeTo(ByteBuffer prefilled)
protected ServerParser getParser()
{
return (ServerParser)super.getParser();
}
@Override
public void onUpgradeTo(ByteBuffer buffer)
{
if (LOG.isDebugEnabled())
LOG.debug("HTTP2 onUpgradeTo {} {}", this, BufferUtil.toDetailString(prefilled));
prefill(prefilled);
LOG.debug("HTTP2 onUpgradeTo {} {}", this, BufferUtil.toDetailString(buffer));
setInputBuffer(buffer);
}
@Override
@ -143,16 +148,19 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
return channel;
}
public boolean upgrade(Request request, HttpFields response101)
public boolean upgrade(Request request)
{
if (HttpMethod.PRI.is(request.getMethod()))
{
((ServerParser)getParser()).directUpgrade();
getParser().directUpgrade();
}
else
{
String value = request.getFields().getField(HttpHeader.HTTP2_SETTINGS).getValue();
final byte[] settings = B64Code.decodeRFC4648URL(value);
HttpField settingsField = request.getFields().getField(HttpHeader.HTTP2_SETTINGS);
if (settingsField == null)
throw new BadMessageException("Missing " + HttpHeader.HTTP2_SETTINGS + " header");
String value = settingsField.getValue();
final byte[] settings = B64Code.decodeRFC4648URL(value == null ? "" : value);
if (LOG.isDebugEnabled())
LOG.debug("{} settings {}",this,TypeUtil.toHexString(settings));

View File

@ -31,33 +31,27 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
public class HTTP2CServer extends Server
{
public HTTP2CServer(int port)
{
// HTTP connector
HttpConfiguration config = new HttpConfiguration();
// HTTP + HTTP/2 connector
ServerConnector http = new ServerConnector(this,new HttpConnectionFactory(config), new HTTP2CServerConnectionFactory(config));
http.setHost("localhost");
http.setPort(port);
http.setIdleTimeout(30000);
// Set the connector
addConnector(http);
// Set a handler
((QueuedThreadPool)getThreadPool()).setName("server");
setHandler(new SimpleHandler());
}
public static void main(String... args ) throws Exception
{
// The Server
HTTP2CServer server = new HTTP2CServer(8080);
// Start the server
server.start();
server.join();
}
private static class SimpleHandler extends AbstractHandler
@ -78,6 +72,5 @@ public class HTTP2CServer extends Server
response.setContentLength(content.length());
response.getOutputStream().print(content);
}
}
}

View File

@ -18,9 +18,7 @@
package org.eclipse.jetty.http2.server;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
@ -45,22 +43,27 @@ import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
public class HTTP2CServerTest extends AbstractServerTest
{
HTTP2CServer _server;
int _port;
private HTTP2CServer _server;
private int _port;
@Before
public void before() throws Exception
{
_server=new HTTP2CServer(0);
_server = new HTTP2CServer(0);
_server.start();
_port=((NetworkConnector)_server.getConnectors()[0]).getLocalPort();
_port = ((NetworkConnector)_server.getConnectors()[0]).getLocalPort();
}
@After
@ -79,8 +82,8 @@ public class HTTP2CServerTest extends AbstractServerTest
String response = IO.toString(client.getInputStream());
assertThat(response,containsString("HTTP/1.1 200 OK"));
assertThat(response,containsString("Hello from Jetty using HTTP/1.0"));
assertThat(response, containsString("HTTP/1.1 200 OK"));
assertThat(response, containsString("Hello from Jetty using HTTP/1.0"));
}
}
@ -95,24 +98,128 @@ public class HTTP2CServerTest extends AbstractServerTest
String response = IO.toString(client.getInputStream());
assertThat(response,containsString("HTTP/1.1 200 OK"));
assertThat(response,containsString("Hello from Jetty using HTTP/1.1"));
assertThat(response,containsString("uri=/one"));
assertThat(response,containsString("uri=/two"));
assertThat(response, containsString("HTTP/1.1 200 OK"));
assertThat(response, containsString("Hello from Jetty using HTTP/1.1"));
assertThat(response, containsString("uri=/one"));
assertThat(response, containsString("uri=/two"));
}
}
@Test
public void testHTTP_2_0_Simple() throws Exception
public void testHTTP_1_1_Upgrade() throws Exception
{
try (Socket client = new Socket("localhost", _port))
{
OutputStream output = client.getOutputStream();
output.write(("" +
"GET /one HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: Upgrade, HTTP2-Settings\r\n" +
"Upgrade: h2c\r\n" +
"HTTP2-Settings: \r\n" +
"\r\n").getBytes(StandardCharsets.ISO_8859_1));
output.flush();
InputStream input = client.getInputStream();
Utf8StringBuilder upgrade = new Utf8StringBuilder();
int crlfs = 0;
while (true)
{
int read = input.read();
if (read == '\r' || read == '\n')
++crlfs;
else
crlfs = 0;
upgrade.append((byte)read);
if (crlfs == 4)
break;
}
assertTrue(upgrade.toString().startsWith("HTTP/1.1 101 "));
byteBufferPool = new MappedByteBufferPool();
generator = new Generator(byteBufferPool);
final AtomicReference<HeadersFrame> headersRef = new AtomicReference<>();
final AtomicReference<DataFrame> dataRef = new AtomicReference<>();
final AtomicReference<CountDownLatch> latchRef = new AtomicReference<>(new CountDownLatch(2));
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter()
{
@Override
public void onHeaders(HeadersFrame frame)
{
headersRef.set(frame);
latchRef.get().countDown();
}
@Override
public void onData(DataFrame frame)
{
dataRef.set(frame);
latchRef.get().countDown();
}
}, 4096, 8192);
parseResponse(client, parser);
Assert.assertTrue(latchRef.get().await(5, TimeUnit.SECONDS));
HeadersFrame response = headersRef.get();
Assert.assertNotNull(response);
MetaData.Response responseMetaData = (MetaData.Response)response.getMetaData();
Assert.assertEquals(200, responseMetaData.getStatus());
DataFrame responseData = dataRef.get();
Assert.assertNotNull(responseData);
String content = BufferUtil.toString(responseData.getData());
// The upgrade request is seen as HTTP/1.1.
assertThat(content, containsString("Hello from Jetty using HTTP/1.1"));
assertThat(content, containsString("uri=/one"));
// Send a HTTP/2 request.
headersRef.set(null);
dataRef.set(null);
latchRef.set(new CountDownLatch(2));
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.control(lease, new PrefaceFrame());
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:" + _port), "/two", HttpVersion.HTTP_2, new HttpFields());
generator.control(lease, new HeadersFrame(3, metaData, null, true));
for (ByteBuffer buffer : lease.getByteBuffers())
output.write(BufferUtil.toArray(buffer));
output.flush();
parseResponse(client, parser);
Assert.assertTrue(latchRef.get().await(5, TimeUnit.SECONDS));
response = headersRef.get();
Assert.assertNotNull(response);
responseMetaData = (MetaData.Response)response.getMetaData();
Assert.assertEquals(200, responseMetaData.getStatus());
responseData = dataRef.get();
Assert.assertNotNull(responseData);
content = BufferUtil.toString(responseData.getData());
assertThat(content, containsString("Hello from Jetty using HTTP/2.0"));
assertThat(content, containsString("uri=/two"));
}
}
@Test
public void testHTTP_2_0_Direct() throws Exception
{
final CountDownLatch latch = new CountDownLatch(3);
byteBufferPool= new MappedByteBufferPool();
byteBufferPool = new MappedByteBufferPool();
generator = new Generator(byteBufferPool);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
generator.control(lease, new PrefaceFrame());
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:"+_port), "/test", HttpVersion.HTTP_2, new HttpFields());
MetaData.Request metaData = new MetaData.Request("GET", HttpScheme.HTTP, new HostPortHttpField("localhost:" + _port), "/test", HttpVersion.HTTP_2, new HttpFields());
generator.control(lease, new HeadersFrame(1, metaData, null, true));
@ -163,8 +270,8 @@ public class HTTP2CServerTest extends AbstractServerTest
String s = BufferUtil.toString(responseData.getData());
assertThat(s,containsString("Hello from Jetty using HTTP/2.0"));
assertThat(s,containsString("uri=/test"));
assertThat(s, containsString("Hello from Jetty using HTTP/2.0"));
assertThat(s, containsString("uri=/test"));
}
}
}

View File

@ -29,10 +29,10 @@ import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpParser.RequestHandler;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.HttpParser.RequestHandler;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
@ -245,7 +245,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
// Continue or break?
else if (filled<=0)
{
if (filled==0)
// Be fill interested only if there was no connection upgrade.
if (filled==0 && getEndPoint().getConnection()==this)
fillInterested();
break;
}