Fixes #3311 - Ability to serve HTTP and HTTPS from the same port.

Introduced PlainOrSslConnectionFactory, to "sniff" the first bytes
on a connection and upgrade to SSL (if the bytes are TLS bytes), or
upgrade to a specific, configured, protocol.

Added also the ability to fail the upgrade in case of a `http`
request to a `https` port and write a minimal response to the client.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2019-01-31 13:23:58 +01:00
parent 169d93e790
commit ccda1ee5f6
5 changed files with 409 additions and 6 deletions

View File

@ -76,7 +76,7 @@ import org.eclipse.jetty.util.thread.Invocable;
* be called again and make another best effort attempt to progress the connection.
*
*/
public class SslConnection extends AbstractConnection
public class SslConnection extends AbstractConnection implements Connection.UpgradeTo
{
private static final Logger LOG = Log.getLogger(SslConnection.class);
private static final String TLS_1_3 = "TLSv1.3";
@ -260,6 +260,19 @@ public class SslConnection extends AbstractConnection
this._allowMissingCloseMessage = allowMissingCloseMessage;
}
private void acquireEncryptedInput()
{
if (_encryptedInput == null)
_encryptedInput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers);
}
@Override
public void onUpgradeTo(ByteBuffer buffer)
{
acquireEncryptedInput();
BufferUtil.append(_encryptedInput, buffer);
}
@Override
public void onOpen()
{
@ -526,8 +539,7 @@ public class SslConnection extends AbstractConnection
throw new IllegalStateException("Unexpected HandshakeStatus " + status);
}
if (_encryptedInput == null)
_encryptedInput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers);
acquireEncryptedInput();
// can we use the passed buffer if it is big enough
ByteBuffer app_in;

View File

@ -50,7 +50,7 @@ import org.eclipse.jetty.util.log.Logger;
/**
* <p>A {@link Connection} that handles the HTTP protocol.</p>
*/
public class HttpConnection extends AbstractConnection implements Runnable, HttpTransport, Connection.UpgradeFrom, WriteFlusher.Listener
public class HttpConnection extends AbstractConnection implements Runnable, HttpTransport, WriteFlusher.Listener, Connection.UpgradeFrom, Connection.UpgradeTo
{
private static final Logger LOG = Log.getLogger(HttpConnection.class);
public static final HttpField CONNECTION_CLOSE = new PreEncodedHttpField(HttpHeader.CONNECTION,HttpHeaderValue.CLOSE.asString());
@ -196,6 +196,12 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
return null;
}
@Override
public void onUpgradeTo(ByteBuffer buffer)
{
BufferUtil.append(getRequestBuffer(), buffer);
}
@Override
public void onFlushed(long bytes) throws IOException
{
@ -500,7 +506,10 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
public void onOpen()
{
super.onOpen();
fillInterested();
if (isRequestBufferEmpty())
fillInterested();
else
getExecutor().execute(this);
}
@Override

View File

@ -0,0 +1,192 @@
//
// ========================================================================
// Copyright (c) 1995-2019 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.server;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* <p>A ConnectionFactory whose connections detect whether the first bytes are
* TLS bytes and upgrades to either a TLS connection or to a plain connection.</p>
*/
public class PlainOrSslConnectionFactory extends AbstractConnectionFactory
{
private static final Logger LOG = Log.getLogger(PlainOrSslConnection.class);
private static final int TLS_ALERT_FRAME_TYPE = 0x15;
private static final int TLS_HANDSHAKE_FRAME_TYPE = 0x16;
private static final int TLS_MAJOR_VERSION = 3;
private final SslConnectionFactory sslConnectionFactory;
private final String plainProtocol;
/**
* <p>Creates a new plain or TLS ConnectionFactory.</p>
* <p>If {@code plainProtocol} is {@code null}, and the first bytes are not TLS, then
* {@link #unknownProtocol(ByteBuffer, EndPoint)} is called; applications may override its
* behavior (by default it closes the EndPoint) for example by writing a minimal response. </p>
*
* @param sslConnectionFactory The SslConnectionFactory to use if the first bytes are TLS
* @param plainProtocol the protocol of the ConnectionFactory to use if the first bytes are not TLS, or null.
*/
public PlainOrSslConnectionFactory(SslConnectionFactory sslConnectionFactory, String plainProtocol)
{
super("plain|ssl");
this.sslConnectionFactory = sslConnectionFactory;
this.plainProtocol = plainProtocol;
}
@Override
public Connection newConnection(Connector connector, EndPoint endPoint)
{
return configure(new PlainOrSslConnection(endPoint, connector), connector, endPoint);
}
/**
* @param buffer The buffer with the first bytes of the connection
* @return whether the bytes seem TLS bytes
*/
protected boolean seemsTLS(ByteBuffer buffer)
{
int tlsFrameType = buffer.get(0) & 0xFF;
int tlsMajorVersion = buffer.get(1) & 0xFF;
return (tlsFrameType == TLS_HANDSHAKE_FRAME_TYPE || tlsFrameType == TLS_ALERT_FRAME_TYPE) && tlsMajorVersion == TLS_MAJOR_VERSION;
}
/**
* <p>Callback method invoked when {@code plainProtocol} is {@code null}
* and the first bytes are not TLS.</p>
* <p>This typically happens when a client is trying to connect to a TLS
* port using the {@code http} scheme (and not the {@code https} scheme).</p>
* <p>This method may be overridden to write back a minimal response such as:</p>
* <pre>
* HTTP/1.1 400 Bad Request
* Content-Length: 35
* Content-Type: text/plain; charset=UTF8
* Connection: close
*
* Plain HTTP request sent to TLS port
* </pre>
*
* @param buffer The buffer with the first bytes of the connection
* @param endPoint The connection EndPoint object
* @see #seemsTLS(ByteBuffer)
*/
protected void unknownProtocol(ByteBuffer buffer, EndPoint endPoint)
{
endPoint.close();
}
private class PlainOrSslConnection extends AbstractConnection implements Connection.UpgradeFrom
{
private final Connector connector;
private final ByteBuffer buffer;
public PlainOrSslConnection(EndPoint endPoint, Connector connector)
{
super(endPoint, connector.getExecutor());
this.connector = connector;
this.buffer = BufferUtil.allocateDirect(1536);
}
@Override
public void onOpen()
{
super.onOpen();
fillInterested();
}
@Override
public void onFillable()
{
try
{
int filled = getEndPoint().fill(buffer);
if (filled > 0)
{
upgrade(buffer);
}
else if (filled == 0)
{
fillInterested();
}
else
{
close();
}
}
catch (IOException x)
{
LOG.warn(x);
close();
}
}
@Override
public ByteBuffer onUpgradeFrom()
{
return buffer;
}
private void upgrade(ByteBuffer buffer)
{
if (LOG.isDebugEnabled())
LOG.debug("Read {}", BufferUtil.toDetailString(buffer));
EndPoint endPoint = getEndPoint();
if (seemsTLS(buffer))
{
if (LOG.isDebugEnabled())
LOG.debug("Detected TLS bytes, upgrading to {}", sslConnectionFactory);
endPoint.upgrade(sslConnectionFactory.newConnection(connector, endPoint));
}
else
{
if (plainProtocol != null)
{
ConnectionFactory connectionFactory = connector.getConnectionFactory(plainProtocol);
if (connectionFactory != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Detected plain bytes, upgrading to {}", connectionFactory);
Connection next = connectionFactory.newConnection(connector, endPoint);
endPoint.upgrade(next);
}
else
{
LOG.warn("Missing {} {} in {}", plainProtocol, ConnectionFactory.class.getSimpleName(), connector);
close();
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Detected plain bytes, but no configured protocol to upgrade to");
unknownProtocol(buffer, endPoint);
}
}
}
}
}

View File

@ -0,0 +1,189 @@
//
// ========================================================================
// Copyright (c) 1995-2019 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.server;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class PlainOrSslConnectionTest
{
private Server server;
private ServerConnector connector;
private void startServer(Function<SslConnectionFactory, PlainOrSslConnectionFactory> configFn, Handler handler) throws Exception
{
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath();
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePath(keystore);
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setKeyManagerPassword("keypwd");
HttpConfiguration httpConfig = new HttpConfiguration();
HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, http.getProtocol());
PlainOrSslConnectionFactory plainOrSsl = configFn.apply(ssl);
connector = new ServerConnector(server, 1, 1, plainOrSsl, ssl, http);
server.addConnector(connector);
server.setHandler(handler);
server.start();
}
@AfterEach
public void stopServer() throws Exception
{
if (server != null)
server.stop();
}
private PlainOrSslConnectionFactory plainOrSsl(SslConnectionFactory ssl)
{
return new PlainOrSslConnectionFactory(ssl, ssl.getNextProtocol());
}
private PlainOrSslConnectionFactory plainToSslWithReport(SslConnectionFactory ssl)
{
return new PlainOrSslConnectionFactory(ssl, null)
{
@Override
protected void unknownProtocol(ByteBuffer buffer, EndPoint endPoint)
{
String response = "" +
"HTTP/1.1 400 Bad Request\r\n" +
"Content-Length: 0\r\n" +
"Connection: close\r\n" +
"\r\n";
Callback.Completable callback = new Callback.Completable();
endPoint.write(callback, ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
callback.whenComplete((r, x) -> endPoint.close());
}
};
}
@Test
public void testPlainOrSslConnection() throws Exception
{
startServer(this::plainOrSsl, new EmptyServerHandler());
String request = "" +
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"\r\n";
byte[] requestBytes = request.getBytes(StandardCharsets.US_ASCII);
// Try first a plain text connection.
try (Socket plain = new Socket())
{
plain.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
OutputStream plainOutput = plain.getOutputStream();
plainOutput.write(requestBytes);
plainOutput.flush();
plain.setSoTimeout(5000);
InputStream plainInput = plain.getInputStream();
HttpTester.Response response = HttpTester.parseResponse(plainInput);
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
// Then try a SSL connection.
SslContextFactory sslContextFactory = new SslContextFactory(true);
sslContextFactory.start();
try (Socket ssl = sslContextFactory.newSslSocket())
{
ssl.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
OutputStream sslOutput = ssl.getOutputStream();
sslOutput.write(requestBytes);
sslOutput.flush();
ssl.setSoTimeout(5000);
InputStream sslInput = ssl.getInputStream();
HttpTester.Response response = HttpTester.parseResponse(sslInput);
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
finally
{
sslContextFactory.stop();
}
}
@Test
public void testPlainToSslWithReport() throws Exception
{
startServer(this::plainToSslWithReport, new EmptyServerHandler());
String request = "" +
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"\r\n";
byte[] requestBytes = request.getBytes(StandardCharsets.US_ASCII);
// Send a plain text HTTP request to SSL port: we should get back a minimal HTTP response.
try (Socket plain = new Socket())
{
plain.connect(new InetSocketAddress("localhost", connector.getLocalPort()), 1000);
OutputStream plainOutput = plain.getOutputStream();
plainOutput.write(requestBytes);
plainOutput.flush();
plain.setSoTimeout(5000);
InputStream plainInput = plain.getInputStream();
HttpTester.Response response = HttpTester.parseResponse(plainInput);
assertNotNull(response);
assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
}
}
private static class EmptyServerHandler extends AbstractHandler.ErrorDispatchHandler
{
@Override
protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
jettyRequest.setHandled(true);
}
}
}

View File

@ -1,4 +1,5 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.server.LEVEL=DEBUG
#org.eclipse.jetty.server.ConnectionLimit.LEVEL=DEBUG
#org.eclipse.jetty.server.AcceptRateLimit.LEVEL=DEBUG