459845 - Support upgrade from http1 to http2/websocket

Added support for unofficial "upgrade" from http/1 to h2c
This commit is contained in:
Greg Wilkins 2015-02-13 12:16:03 +11:00
parent 0f7ee8d607
commit c3332e7d2e
16 changed files with 252 additions and 57 deletions

View File

@ -1604,7 +1604,7 @@ public class HttpParserTest
"SM\015\012"+
"\015\012");
HttpParser.RequestHandler<ByteBuffer> handler = new Handler();
HttpParser.RequestHandler handler = new Handler();
HttpParser parser= new HttpParser(handler);
parseAll(parser,buffer);

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.http2.client;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.Executor;

View File

@ -26,6 +26,7 @@ import java.util.concurrent.Executor;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.ConcurrentArrayQueue;
@ -33,7 +34,7 @@ import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ExecutionStrategy;
public class HTTP2Connection extends AbstractConnection
public class HTTP2Connection extends AbstractConnection implements Connection.UpgradeTo
{
protected static final Logger LOG = Log.getLogger(HTTP2Connection.class);
@ -42,7 +43,8 @@ public class HTTP2Connection extends AbstractConnection
private final Parser parser;
private final ISession session;
private final int bufferSize;
private final ExecutionStrategy executionStrategy; // TODO: make it pluggable from outside ?
private final HTTP2Producer producer = new HTTP2Producer();
private final ExecutionStrategy executionStrategy;
public HTTP2Connection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, Parser parser, ISession session, int bufferSize)
{
@ -51,7 +53,7 @@ public class HTTP2Connection extends AbstractConnection
this.parser = parser;
this.session = session;
this.bufferSize = bufferSize;
this.executionStrategy = ExecutionStrategy.Factory.instanceFor(new HTTP2Producer(), executor);
this.executionStrategy = ExecutionStrategy.Factory.instanceFor(producer, executor);
}
protected ISession getSession()
@ -59,13 +61,18 @@ public class HTTP2Connection extends AbstractConnection
return session;
}
public void onUpgradeTo(ByteBuffer prefilled)
{
producer.prefill(prefilled);
}
@Override
public void onOpen()
{
if (LOG.isDebugEnabled())
LOG.debug("HTTP2 Open {} ", this);
super.onOpen();
fillInterested();
executionStrategy.execute();
}
@Override
@ -166,6 +173,11 @@ public class HTTP2Connection extends AbstractConnection
}
}
public void prefill(ByteBuffer prefilledBuffer)
{
buffer=prefilledBuffer;
}
private void release()
{
if (BufferUtil.isEmpty(buffer))

View File

@ -38,6 +38,20 @@ public class PrefaceParser
this.listener = listener;
}
/* ------------------------------------------------------------ */
/** Unsafe upgrade is an unofficial upgrade from HTTP/1.0 to HTTP/2.0
* initiated when a the {@link HttpConnection} 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.
*/
public void directUpgrade()
{
if (cursor!=0)
throw new IllegalStateException();
cursor=18;
}
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())

View File

@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
@ -41,6 +42,20 @@ 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 {@link HttpConnection} 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.
*/
public void directUpgrade()
{
if (state!=State.PREFACE)
throw new IllegalStateException();
prefaceParser.directUpgrade();
}
@Override
public void parse(ByteBuffer buffer)
{

View File

@ -42,21 +42,15 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
public AbstractHTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration)
{
super("h2-17","h2-16","h2-15","h2-14","h2");
this(httpConfiguration,"h2-17","h2-16","h2-15","h2-14","h2");
}
protected AbstractHTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration,String... protocols)
{
super(protocols);
this.httpConfiguration = httpConfiguration;
}
@Deprecated
public boolean isDispatchIO()
{
return false;
}
@Deprecated
public void setDispatchIO(boolean dispatchIO)
{
}
public int getMaxDynamicTableSize()
{
return maxDynamicTableSize;
@ -108,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.getByteBufferPool(), session);
Parser parser = newServerParser(connector, session);
HTTP2Connection connection = new HTTP2ServerConnection(connector.getByteBufferPool(), connector.getExecutor(),
endPoint, httpConfiguration, parser, session, getInputBufferSize(), listener);
@ -122,5 +116,8 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
protected abstract ServerSessionListener newSessionListener(Connector connector, EndPoint endPoint);
protected abstract ServerParser newServerParser(ByteBufferPool byteBufferPool, ServerParser.Listener listener);
protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener)
{
return new ServerParser(connector.getByteBufferPool(), listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize());
}
}

View File

@ -0,0 +1,53 @@
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http2.server;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
public class HTTP2CServerConnectionFactory extends HTTP2ServerConnectionFactory
{
public HTTP2CServerConnectionFactory(HttpConfiguration httpConfiguration)
{
super(httpConfiguration,"h2c");
}
@Override
public boolean isAcceptable(String protocol, String tlsProtocol, String tlsCipher)
{
// Never use TLS with h2c
return false;
}
protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener)
{
ServerParser parser = super.newServerParser(connector,listener);
if (connector.getDefaultConnectionFactory() instanceof HttpConnectionFactory)
{
// This must be a sneaky upgrade from HTTP/1
// So advance the parsers pointer until after the PRI * HTTP/2.0 request.
parser.directUpgrade();
}
return parser;
}
}

View File

@ -51,6 +51,11 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
{
super(httpConfiguration);
}
protected HTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration,String... protocols)
{
super(httpConfiguration,protocols);
}
@Override
protected ServerSessionListener newSessionListener(Connector connector, EndPoint endPoint)
@ -58,12 +63,6 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
return new HTTPServerSessionListener(connector, endPoint);
}
@Override
protected ServerParser newServerParser(ByteBufferPool byteBufferPool, ServerParser.Listener listener)
{
return new ServerParser(byteBufferPool, listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize());
}
@Override
public boolean isAcceptable(String protocol, String tlsProtocol, String tlsCipher)
{

View File

@ -19,8 +19,6 @@
package org.eclipse.jetty.http2.server;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
@ -40,10 +38,4 @@ public class RawHTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnecti
{
return listener;
}
@Override
protected ServerParser newServerParser(ByteBufferPool byteBufferPool, ServerParser.Listener listener)
{
return new ServerParser(byteBufferPool, listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize());
}
}

View File

@ -0,0 +1,82 @@
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http2.server;
import java.io.IOException;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
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.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
public class HTTP2CServer
{
public static void main( String[] args ) throws Exception
{
// The Server
Server server = new Server();
// HTTP connector
HttpConfiguration config = new HttpConfiguration();
ServerConnector http = new ServerConnector(server,new HttpConnectionFactory(config), new HTTP2CServerConnectionFactory(config));
http.setHost("localhost");
http.setPort(8080);
http.setIdleTimeout(30000);
// Set the connector
server.addConnector(http);
// Set a handler
server.setHandler(new SimpleHandler());
// Start the server
server.start();
server.join();
}
private static class SimpleHandler extends AbstractHandler
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
String code=request.getParameter("code");
if (code!=null)
response.setStatus(Integer.parseInt(code));
response.setHeader("Custom","Value");
response.setContentType("text/plain");
String content = "Hello from Jetty using "+request.getProtocol() +"\n";
content+="uri="+request.getRequestURI()+"\n";
content+="date="+new Date()+"\n";
response.setContentLength(content.length());
response.getOutputStream().print(content);
}
}
}

View File

@ -18,13 +18,14 @@
package org.eclipse.jetty.io;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

View File

@ -182,7 +182,7 @@ public abstract class AbstractEndPoint extends IdleTimeout implements EndPoint
?((Connection.UpgradeFrom)old_connection).onUpgradeFrom():null;
old_connection.onClose();
old_connection.getEndPoint().setConnection(newConnection);
if (newConnection instanceof Connection.UpgradeTo)
((Connection.UpgradeTo)newConnection).onUpgradeTo(prefilled);
else if (BufferUtil.hasContent(prefilled))

View File

@ -277,7 +277,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
{
case REQUEST_DISPATCH:
if (!_request.hasMetaData())
throw new IllegalStateException();
throw new IllegalStateException("state="+_state);
_request.setHandled(false);
_response.getHttpOutput().reopen();
_request.setDispatcherType(DispatcherType.REQUEST);

View File

@ -34,6 +34,8 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -48,7 +50,7 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
private final HttpConnection _httpConnection;
private final HttpFields _fields = new HttpFields();
private HttpField _connection;
private boolean _expect = false;
private boolean _unknownExpectation = false;
private boolean _expect100Continue = false;
private boolean _expect102Processing = false;
@ -76,7 +78,7 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
public void recycle()
{
super.recycle();
_expect = false;
_unknownExpectation = false;
_expect100Continue = false;
_expect102Processing = false;
_metadata.recycle();
@ -102,7 +104,7 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
_metadata.setMethod(method);
_metadata.getURI().parse(uri);
_metadata.setHttpVersion(version);
_expect = false;
_unknownExpectation = false;
_expect100Continue = false;
_expect102Processing = false;
return false;
@ -150,7 +152,7 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
{
expect = HttpHeaderValue.CACHE.get(values[i].trim());
if (expect == null)
_expect = true;
_unknownExpectation = true;
else
{
switch (expect)
@ -162,7 +164,7 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
_expect102Processing = true;
break;
default:
_expect = true;
_unknownExpectation = true;
}
}
}
@ -270,10 +272,10 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
case HTTP_1_1:
{
if (_expect)
if (_unknownExpectation)
{
badMessage(HttpStatus.EXPECTATION_FAILED_417,null);
return true;
return false;
}
if (_connection!=null)
@ -293,6 +295,37 @@ class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandl
break;
}
case HTTP_2:
{
// Allow sneaky "upgrade" to HTTP_2_0 only if the connector supports h2, but not protocol negotiation
ConnectionFactory h2=null;
if (!(getConnector().getDefaultConnectionFactory() instanceof NegotiatingServerConnectionFactory))
for (ConnectionFactory factory : getConnector().getConnectionFactories())
if (factory.getProtocols().contains("h2c"))
h2=factory;
// If now a sneaky "upgrade" then a real upgrade is required
if (h2==null ||
_metadata.getMethod()!=HttpMethod.PRI.asString() ||
!"*".equals(_metadata.getURI().toString()) ||
_fields.size()>0)
{
badMessage(HttpStatus.UPGRADE_REQUIRED_426,null);
return false;
}
// Do a direct upgrade. Even though this is a HTTP/1 connector, we have seen a
// HTTP/2.0 prefix, so let the request through
Connection old_connection=getEndPoint().getConnection();
Connection new_connection = h2.newConnection(getConnector(),getEndPoint());
if (LOG.isDebugEnabled())
LOG.debug("Direct Upgrade from {} to {}", old_connection,new_connection);
getResponse().setStatus(101); // This will not get sent
getRequest().setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE,new_connection);
getHttpTransport().onCompleted();
return true;
}
default:
{
throw new IllegalStateException();

View File

@ -363,6 +363,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
Connection connection = (Connection)_channel.getRequest().getAttribute(UPGRADE_CONNECTION_ATTRIBUTE);
if (connection != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Upgrade from {} to {}", this, connection);
_channel.getState().upgrade();
getEndPoint().upgrade(connection);
_channel.recycle();
@ -535,12 +537,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
public void failed(Throwable x)
{
_input.failed(x);
}
case HTTP_2:
{
persistent=false;
badMessage(400,null);
return true;
}
}

View File

@ -201,14 +201,8 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory
}
EndPoint endPoint = new ProxyEndPoint(getEndPoint(),remote,local);
Connection oldConnection = getEndPoint().getConnection();
Connection newConnection = connectionFactory.newConnection(_connector, endPoint);
if (LOG.isDebugEnabled())
LOG.debug("Switching to {} {}", _next, getEndPoint());
oldConnection.onClose();
endPoint.setConnection(newConnection);
newConnection.onOpen();
Connection newConnection = connectionFactory.newConnection(_connector, endPoint);
endPoint.upgrade(newConnection);
}
catch (Throwable e)
{
@ -333,5 +327,11 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory
{
_endp.onClose();
}
@Override
public void upgrade(Connection newConnection)
{
_endp.upgrade(newConnection);
}
}
}