Как я могу обслуживать https и http для Jetty из одного порта?
(Я знаю, что это повторяющийся вопрос, но оригинальный плакат спросил его по неправильной причине. Я не подразумеваю, что я прошу его по правильной причине, но посмотрим.)
У нас есть веб-сервис, который работает на нестандартном номере порта. Даже если пользователи, похоже, помнят номер порта, иногда они набирают http: вместо https: по ошибке. Кто-то спрашивает, можем ли мы обслуживать HTTP на этом порту, а затем перенаправить их на HTTPS на тот же порт. Это звучит плохо... Мне нравится удобство использования, но похоже, что это должно быть работой браузера для этого?
Единственное решение, которое я видел, это "написать свой собственный прокси-сервер перед Jetty". Это решение будет работать, но я не думаю, что он будет работать хорошо, поскольку я не уверен, что могу написать прокси, который так же эффективен, как и Jetty. Кроме того, даже если сам прокси-сервер эффективен, все данные по-прежнему должны будут иметь дополнительный прыжок, что гарантированно замедлит трафик в любом случае.
Есть ли лучший способ, чем это? Возможно, у Jetty есть место, где логика обнаружения протокола может быть вклинирована, что позволит использовать их скорость, а также удалить дополнительный прыжок, который представит прокси.
Ответы
Ответ 1
Обновление: см. этот ответ для получения инструкций о том, как перенаправить один порт на HTTPS и HTTP-прослушиватель. Если по какой-либо причине вы не используете это решение, см. Ниже:
Невозможно передать трафик с HTTP и https на один и тот же порт. Jetty использует два совершенно разных разъема для привязки к безопасным и незащищенным портам. Фактически, каждый веб-сервер, с которым я столкнулся, связывает два протокола с двумя полностью отдельными портами.
Одна вещь, которую я бы предложил для удобства использования, - использовать порты по умолчанию, которые полностью скрывают порт от пользователя. По умолчанию http использует порт 80, а по умолчанию https использует порт 443. Поэтому, если вы сконфигурируете свои соединители для работы на порту 80 и в порту 443 соответственно, то вашим пользователям не нужно вводить порт, а ваша команда разработчиков не должны обрабатывать номера портов в абсолютных путях в HTML, CSS, JavaScript и других ресурсах.
Jetty предназначен для автономного веб-сервера, в отличие от старых версий Tomcat, которые Apache предлагает запустить за HTTP-сервером Apache. Поэтому, пока у вас нет другого сервера HTTP-сервера, и используя эти порты, чтобы вы не могли, вы должны иметь возможность настроить Jetty для запуска на портах по умолчанию без каких-либо проблем. Это происходит из опыта. Мы запускаем Jetty именно таким образом.
Наконец, протокол может быть привязан к нескольким портам. Таким образом, если вы в настоящее время используете Jetty на портах 8080 для http и 8443 для https, вы можете оставить эти разъемы активными и добавить еще два разъема для порта 80 и порта 443. Это обеспечило обратную совместимость для части вашего приложения, которая по-прежнему используя номера портов и дает вам время для этого.
<!-- Legacy HTTP connector -->
<Call name="addConnector">
<Arg>
<New class="org.mortbay.jetty.nio.SelectChannelConnector">
<Set name="host"><SystemProperty name="jetty.host" /></Set>
<Set name="port"><SystemProperty name="jetty.port" default="8080"/></Set>
<Set name="maxIdleTime">30000</Set>
<Set name="Acceptors">2</Set>
<Set name="statsOn">false</Set>
<Set name="confidentialPort">8443</Set>
<Set name="lowResourcesConnections">5000</Set>
<Set name="lowResourcesMaxIdleTime">5000</Set>
</New>
</Arg>
</Call>
<!-- Second connector for http on port 80 -->
<Call name="addConnector">
<Arg>
<New class="org.mortbay.jetty.nio.SelectChannelConnector">
<Set name="host"><SystemProperty name="jetty.host" /></Set>
<Set name="port"><SystemProperty name="jetty.port" default="80"/></Set>
<Set name="maxIdleTime">30000</Set>
<Set name="Acceptors">2</Set>
<Set name="statsOn">false</Set>
<Set name="confidentialPort">8443</Set>
<Set name="lowResourcesConnections">5000</Set>
<Set name="lowResourcesMaxIdleTime">5000</Set>
</New>
</Arg>
</Call>
<!-- Legacy SSL Connector for https port 8443 -->
<Call name="addConnector">
<Arg>
<New class="org.mortbay.jetty.security.SslSocketConnector">
<Set name="Port">8443</Set>
<Set name="maxIdleTime">30000</Set>
<Set name="handshakeTimeout">2000</Set>
<Set name="keystore"><SystemProperty name="jetty.home" default="." />/etc/keystore</Set>
<Set name="password">xxxxxx</Set>
<Set name="keyPassword">xxxxxx</Set>
<Set name="truststore"><SystemProperty name="jetty.home" default="." />/etc/keystore</Set>
<Set name="trustPassword">OBF:xxxxx</Set>
<Set name="handshakeTimeout">2000</Set>
<!-- Set name="ThreadPool">
<New class="org.mortbay.thread.BoundedThreadPool">
<Set name="minThreads">10</Set>
<Set name="maxThreads">250</Set>
</New>
</Set -->
</New>
</Arg>
</Call>
<!-- Default SSL Connector for https port 443 -->
<Call name="addConnector">
<Arg>
<New class="org.mortbay.jetty.security.SslSocketConnector">
<Set name="Port">443</Set>
<Set name="maxIdleTime">30000</Set>
<Set name="handshakeTimeout">2000</Set>
<Set name="keystore"><SystemProperty name="jetty.home" default="." />/etc/keystore</Set>
<Set name="password">xxxxxx</Set>
<Set name="keyPassword">xxxxxx</Set>
<Set name="truststore"><SystemProperty name="jetty.home" default="." />/etc/keystore</Set>
<Set name="trustPassword">OBF:xxxxx</Set>
<Set name="handshakeTimeout">2000</Set>
<!-- Set name="ThreadPool">
<New class="org.mortbay.thread.BoundedThreadPool">
<Set name="minThreads">10</Set>
<Set name="maxThreads">250</Set>
</New>
</Set -->
</New>
</Arg>
</Call>
Для 2-го и 4-го разъемов единственными реальными отличиями являются номера портов. Короче говоря, вы можете настроить несколько портов на один коннектор/протокол, но вы не можете настроить несколько протоколов/разъемов для одного и того же порта.
Ответ 2
Да, мы можем
Это возможно, и мы это сделали. Код здесь работает с Jetty 8; Я не тестировал с Jetty 9, но этот ответ имеет аналогичный код для Jetty 9.
Кстати, это называется унификацией портов, и оно, по-видимому, долгое время поддерживалось в Glassfish с помощью Grizzly.
Структура
Основная идея - создать реализацию org.eclipse.jetty.server.Connector
, которая может смотреть вперёд на первый байт клиентского запроса. К счастью, как HTTP, так и HTTPS клиент запускает связь. Для HTTPS (и обычно TLS/SSL) первый байт будет 0x16
(TLS) или >= 0x80
(SSLv2). Для HTTP первый байт будет добрым печатным 7-битным ASCII. Теперь, в зависимости от первого байта, Connector
будет либо создавать SSL-соединение, либо простое соединение.
В этом коде мы используем тот факт, что Jetty SslSelectChannelConnector
сам расширяет SelectChannelConnector
и имеет метод newPlainConnection()
(вызов его суперкласса для создания не SSL-соединения), а также newConnection()
метод (для создания SSL-соединения). Таким образом, наш новый Connector
может расширять SslSelectChannelConnector
и делегировать один из этих методов после наблюдения первого байта от клиента.
К сожалению, мы должны будем создать экземпляр AsyncConnection
до того, как будет доступен первый байт. Некоторые методы этого экземпляра могут даже вызываться до того, как будет доступен первый байт. Таким образом, мы создаем LazyConnection implements AsyncConnection
, который позже может определить, какое соединение он будет делегировать, или даже вернуть чувствительные ответы по умолчанию на некоторые методы, прежде чем он узнает.
Будучи основанным на NIO, наш Connector
будет работать с SocketChannel
. К счастью, мы можем расширить SocketChannel
, чтобы создать ReadAheadSocketChannelWrapper
, который делегирует "реальный" SocketChannel
, но может проверять и хранить первые байты клиентского сообщения.
Некоторые детали
Один очень взломанный бит. Одним из методов, который должен выполнить наш Connector
, является customize(Endpoint,Request)
. Если мы закончим с использованием SSL Endpoint
, мы можем просто перейти к нашему суперклассу; в противном случае суперкласс будет бросать ClassCastException
, но только после перехода к его суперклассу и установки схемы на Request
. Поэтому мы переходим к суперклассу, но отменяем настройку схемы, когда видим исключение.
Мы также переопределяем isConfidential()
и isIntegral()
, чтобы наши сервлеты могли правильно использовать HttpServletRequest.isSecure()
для определения того, использовался ли HTTP или HTTPS.
Попытка прочитать первый байт от клиента может бросить IOException
, но нам, возможно, придется попытаться это в месте, где IOException
не ожидается, и в этом случае мы сохраняем исключение и бросаем его позже.
Расширение SocketChannel
выглядит по-разному в Java >= 7 и Java 6. В последнем случае просто закомментируйте методы, которые не имеют Java 6 SocketChannel
.
Код
public class PortUnificationSelectChannelConnector extends SslSelectChannelConnector {
public PortUnificationSelectChannelConnector() {
super();
}
public PortUnificationSelectChannelConnector(SslContextFactory sslContextFactory) {
super(sslContextFactory);
}
@Override
protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key) throws IOException {
return super.newEndPoint(new ReadAheadSocketChannelWrapper(channel, 1), selectSet, key);
}
@Override
protected AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endPoint) {
return new LazyConnection((ReadAheadSocketChannelWrapper)channel, endPoint);
}
@Override
public void customize(EndPoint endpoint, Request request) throws IOException {
String scheme = request.getScheme();
try {
super.customize(endpoint, request);
} catch (ClassCastException e) {
request.setScheme(scheme);
}
}
@Override
public boolean isConfidential(Request request) {
if (request.getAttribute("javax.servlet.request.cipher_suite") != null) return true;
else return isForwarded() && request.getScheme().equalsIgnoreCase(HttpSchemes.HTTPS);
}
@Override
public boolean isIntegral(Request request) {
return isConfidential(request);
}
class LazyConnection implements AsyncConnection {
private final ReadAheadSocketChannelWrapper channel;
private final AsyncEndPoint endPoint;
private final long timestamp;
private AsyncConnection connection;
public LazyConnection(ReadAheadSocketChannelWrapper channel, AsyncEndPoint endPoint) {
this.channel = channel;
this.endPoint = endPoint;
this.timestamp = System.currentTimeMillis();
this.connection = determineNewConnection(channel, endPoint, false);
}
public Connection handle() throws IOException {
if (connection == null) {
connection = determineNewConnection(channel, endPoint, false);
channel.throwPendingException();
}
if (connection != null) return connection.handle();
else return this;
}
public long getTimeStamp() {
return timestamp;
}
public void onInputShutdown() throws IOException {
if (connection == null) connection = determineNewConnection(channel, endPoint, true);
connection.onInputShutdown();
}
public boolean isIdle() {
if (connection == null) connection = determineNewConnection(channel, endPoint, false);
if (connection != null) return connection.isIdle();
else return false;
}
public boolean isSuspended() {
if (connection == null) connection = determineNewConnection(channel, endPoint, false);
if (connection != null) return connection.isSuspended();
else return false;
}
public void onClose() {
if (connection == null) connection = determineNewConnection(channel, endPoint, true);
connection.onClose();
}
public void onIdleExpired(long l) {
if (connection == null) connection = determineNewConnection(channel, endPoint, true);
connection.onIdleExpired(l);
}
AsyncConnection determineNewConnection(ReadAheadSocketChannelWrapper channel, AsyncEndPoint endPoint, boolean force) {
byte[] bytes = channel.getBytes();
if ((bytes == null || bytes.length == 0) && !force) return null;
if (looksLikeSsl(bytes)) {
return PortUnificationSelectChannelConnector.super.newConnection(channel, endPoint);
} else {
return PortUnificationSelectChannelConnector.super.newPlainConnection(channel, endPoint);
}
}
// TLS first byte is 0x16
// SSLv2 first byte is >= 0x80
// HTTP is guaranteed many bytes of ASCII
private boolean looksLikeSsl(byte[] bytes) {
if (bytes == null || bytes.length == 0) return false; // force HTTP
byte b = bytes[0];
return b >= 0x7F || (b < 0x20 && b != '\n' && b != '\r' && b != '\t');
}
}
static class ReadAheadSocketChannelWrapper extends SocketChannel {
private final SocketChannel channel;
private final ByteBuffer start;
private byte[] bytes;
private IOException pendingException;
private int leftToRead;
public ReadAheadSocketChannelWrapper(SocketChannel channel, int readAheadLength) throws IOException {
super(channel.provider());
this.channel = channel;
start = ByteBuffer.allocate(readAheadLength);
leftToRead = readAheadLength;
readAhead();
}
public synchronized void readAhead() throws IOException {
if (leftToRead > 0) {
int n = channel.read(start);
if (n == -1) {
leftToRead = -1;
} else {
leftToRead -= n;
}
if (leftToRead <= 0) {
start.flip();
bytes = new byte[start.remaining()];
start.get(bytes);
start.rewind();
}
}
}
public byte[] getBytes() {
if (pendingException == null) {
try {
readAhead();
} catch (IOException e) {
pendingException = e;
}
}
return bytes;
}
public void throwPendingException() throws IOException {
if (pendingException != null) {
IOException e = pendingException;
pendingException = null;
throw e;
}
}
private int readFromStart(ByteBuffer dst) throws IOException {
int sr = start.remaining();
int dr = dst.remaining();
if (dr == 0) return 0;
int n = Math.min(dr, sr);
dst.put(bytes, start.position(), n);
start.position(start.position() + n);
return n;
}
public synchronized int read(ByteBuffer dst) throws IOException {
throwPendingException();
readAhead();
if (leftToRead > 0) return 0;
int sr = start.remaining();
if (sr > 0) {
int n = readFromStart(dst);
if (n < sr) return n;
}
return sr + channel.read(dst);
}
public synchronized long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
throwPendingException();
if (offset + length > dsts.length || length < 0 || offset < 0) {
throw new IndexOutOfBoundsException();
}
readAhead();
if (leftToRead > 0) return 0;
int sr = start.remaining();
int newOffset = offset;
if (sr > 0) {
int accum = 0;
for (; newOffset < offset + length; newOffset++) {
accum += readFromStart(dsts[newOffset]);
if (accum == sr) break;
}
if (accum < sr) return accum;
}
return sr + channel.read(dsts, newOffset, length - newOffset + offset);
}
public int hashCode() {
return channel.hashCode();
}
public boolean equals(Object obj) {
return channel.equals(obj);
}
public String toString() {
return channel.toString();
}
public Socket socket() {
return channel.socket();
}
public boolean isConnected() {
return channel.isConnected();
}
public boolean isConnectionPending() {
return channel.isConnectionPending();
}
public boolean connect(SocketAddress remote) throws IOException {
return channel.connect(remote);
}
public boolean finishConnect() throws IOException {
return channel.finishConnect();
}
public int write(ByteBuffer src) throws IOException {
return channel.write(src);
}
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
return channel.write(srcs, offset, length);
}
@Override
protected void implCloseSelectableChannel() throws IOException {
channel.close();
}
@Override
protected void implConfigureBlocking(boolean block) throws IOException {
channel.configureBlocking(block);
}
// public SocketAddress getLocalAddress() throws IOException {
// return channel.getLocalAddress();
// }
//
// public <T> T getOption(java.net.SocketOption<T> name) throws IOException {
// return channel.getOption(name);
// }
//
// public Set<java.net.SocketOption<?>> supportedOptions() {
// return channel.supportedOptions();
// }
//
// public SocketChannel bind(SocketAddress local) throws IOException {
// return channel.bind(local);
// }
//
// public SocketAddress getRemoteAddress() throws IOException {
// return channel.getRemoteAddress();
// }
//
// public <T> SocketChannel setOption(java.net.SocketOption<T> name, T value) throws IOException {
// return channel.setOption(name, value);
// }
//
// public SocketChannel shutdownInput() throws IOException {
// return channel.shutdownInput();
// }
//
// public SocketChannel shutdownOutput() throws IOException {
// return channel.shutdownOutput();
// }
}
}
Ответ 3
Основываясь на ответе "Да, мы можем", я создаю код, который работает с текущим причалом 9.3.11, и я думаю, что некоторые из них будут заинтересованы.
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ReadPendingException;
import java.nio.channels.WritePendingException;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
public class MyReadAheadEndpoint implements EndPoint {
/** real endpoint we are wrapping */ private final EndPoint endPoint;
/** buffer used to read start bytes */ private final ByteBuffer start ;
/** how many N start bytes to read */ private int leftToRead;
/** first N bytes */ private final byte[] bytes ;
/** buffered exception to throw next */ private IOException pendingException = null;
@Override public InetSocketAddress getLocalAddress () { return endPoint.getLocalAddress(); }
@Override public InetSocketAddress getRemoteAddress () { return endPoint.getRemoteAddress(); }
@Override public boolean isOpen () { return endPoint.isOpen(); }
@Override public long getCreatedTimeStamp () { return endPoint.getCreatedTimeStamp(); }
@Override public boolean isOutputShutdown () { return endPoint.isOutputShutdown(); }
@Override public boolean isInputShutdown () { return endPoint.isInputShutdown(); }
@Override public void shutdownOutput () { endPoint.shutdownOutput(); }
@Override public void close () { endPoint.close(); }
@Override public Object getTransport () { return endPoint.getTransport(); }
@Override public long getIdleTimeout () { return endPoint.getIdleTimeout(); }
@Override public Connection getConnection () { return endPoint.getConnection(); }
@Override public void onOpen () { endPoint.onOpen(); }
@Override public void onClose () { endPoint.onClose(); }
@Override public boolean isOptimizedForDirectBuffers() { return endPoint.isOptimizedForDirectBuffers(); }
@Override public boolean isFillInterested () { return endPoint.isFillInterested(); }
@Override public boolean flush (final ByteBuffer... v) throws IOException { return endPoint.flush(v); }
@Override public void setIdleTimeout (final long v) { endPoint.setIdleTimeout(v); }
@Override public void write (final Callback v, final ByteBuffer... b) throws WritePendingException { endPoint.write(v, b); }
@Override public void setConnection (final Connection v) { endPoint.setConnection(v); }
@Override public void upgrade (final Connection v) { endPoint.upgrade(v); }
@Override public void fillInterested (final Callback v) throws ReadPendingException { endPoint.fillInterested(v); }
@Override public int hashCode() { return endPoint.hashCode(); }
@Override public boolean equals(final Object obj) { return endPoint.equals(obj); }
@Override public String toString() { return endPoint.toString(); }
public byte[] getBytes() { if (pendingException == null) { try { readAhead(); } catch (final IOException e) { pendingException = e; } } return bytes; }
private void throwPendingException() throws IOException { if (pendingException != null) { final IOException e = pendingException; pendingException = null; throw e; } }
public MyReadAheadEndpoint(final EndPoint channel, final int readAheadLength){
this.endPoint = channel;
start = ByteBuffer.wrap(bytes = new byte[readAheadLength]);
start.flip();
leftToRead = readAheadLength;
}
private synchronized void readAhead() throws IOException {
if (leftToRead > 0) {
final int n = endPoint.fill(start);
if (n == -1) { leftToRead = -1; }
else { leftToRead -= n; }
if (leftToRead <= 0) start.rewind();
}
}
private int readFromStart(final ByteBuffer dst) throws IOException {
final int n = Math.min(dst.remaining(), start.remaining());
if (n > 0) {
dst.put(bytes, start.position(), n);
start.position(start.position() + n);
dst.flip();
}
return n;
}
@Override public synchronized int fill(final ByteBuffer dst) throws IOException {
throwPendingException();
if (leftToRead > 0) readAhead();
if (leftToRead > 0) return 0;
final int sr = start.remaining();
if (sr > 0) {
dst.compact();
final int n = readFromStart(dst);
if (n < sr) return n;
}
return sr + endPoint.fill(dst);
}
}
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.annotation.Name;
public class MySslConnectionFactory extends AbstractConnectionFactory {
private final SslContextFactory _sslContextFactory;
private final String _nextProtocol;
public MySslConnectionFactory() { this(HttpVersion.HTTP_1_1.asString()); }
public MySslConnectionFactory(@Name("next") final String nextProtocol) { this((SslContextFactory)null, nextProtocol); }
public MySslConnectionFactory(@Name("sslContextFactory") final SslContextFactory factory, @Name("next") final String nextProtocol) {
super("SSL");
this._sslContextFactory = factory == null?new SslContextFactory():factory;
this._nextProtocol = nextProtocol;
this.addBean(this._sslContextFactory);
}
public SslContextFactory getSslContextFactory() { return this._sslContextFactory; }
@Override protected void doStart() throws Exception {
super.doStart();
final SSLEngine engine = this._sslContextFactory.newSSLEngine();
engine.setUseClientMode(false);
final SSLSession session = engine.getSession();
if(session.getPacketBufferSize() > this.getInputBufferSize()) this.setInputBufferSize(session.getPacketBufferSize());
}
@Override public Connection newConnection(final Connector connector, final EndPoint realEndPoint) {
final MyReadAheadEndpoint aheadEndpoint = new MyReadAheadEndpoint(realEndPoint, 1);
final byte[] bytes = aheadEndpoint.getBytes();
final boolean isSSL;
if (bytes == null || bytes.length == 0) {
System.out.println("NO-Data in newConnection : "+aheadEndpoint.getRemoteAddress());
isSSL = true;
} else {
final byte b = bytes[0]; // TLS first byte is 0x16 , SSLv2 first byte is >= 0x80 , HTTP is guaranteed many bytes of ASCII
isSSL = b >= 0x7F || (b < 0x20 && b != '\n' && b != '\r' && b != '\t');
if(!isSSL) System.out.println("newConnection["+isSSL+"] : "+aheadEndpoint.getRemoteAddress());
}
final EndPoint plainEndpoint;
final SslConnection sslConnection;
if (isSSL) {
final SSLEngine engine = this._sslContextFactory.newSSLEngine(aheadEndpoint.getRemoteAddress());
engine.setUseClientMode(false);
sslConnection = this.newSslConnection(connector, aheadEndpoint, engine);
sslConnection.setRenegotiationAllowed(this._sslContextFactory.isRenegotiationAllowed());
this.configure(sslConnection, connector, aheadEndpoint);
plainEndpoint = sslConnection.getDecryptedEndPoint();
} else {
sslConnection = null;
plainEndpoint = aheadEndpoint;
}
final ConnectionFactory next = connector.getConnectionFactory(_nextProtocol);
final Connection connection = next.newConnection(connector, plainEndpoint);
plainEndpoint.setConnection(connection);
return sslConnection == null ? connection : sslConnection;
}
protected SslConnection newSslConnection(final Connector connector, final EndPoint endPoint, final SSLEngine engine) {
return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine);
}
@Override public String toString() {
return String.format("%[email protected]%x{%s->%s}", new Object[]{this.getClass().getSimpleName(), Integer.valueOf(this.hashCode()), this.getProtocol(), this._nextProtocol});
}
}
Ответ 4
Вы можете реализовать это, написав собственный Jetty ConnectionFactory. Я бы предложил начать с копирования и изменения кода SslConnectionFactory и SslConnection. Вам нужно проверить первые несколько байтов соединения (буферизацию при необходимости), чтобы искать SSL Client Hello. С помощью SSLv2 Hello вы можете определить, что на два байта длины, а затем на 0x01, а затем на байты версии. SSLv3 Hello начинается с 0x16, за которым следуют байты версии. Последовательность байтов версии будет 0x03 0x00 для SSL 3.0, 0x02 0x00 для SSL 2.0, 0x03 0x01 для TLS 1.0, 0x03 0x02 для TLS 1.1, 0x03 0x03 для TLS 1.2. Действительный HTTP-трафик никогда не должен начинаться с этих байтовых последовательностей. (Этот ответ содержит более подробную информацию.) Если это SSL, пройдите через SSLEngine; если нет, передайте его непосредственно на следующий соединитель протокола.
Ответ 5
Даже принимая Jetty из изображения, это не реально, потому что серверу придется определять, является ли входящее соединение HTTP или SSL/TLS. Протокол TLS не предназначен для поддержки этого использования, поэтому любая реализация будет хаком (и я тоже не могу найти).
Существует мультиплексор SSL-SSH, который может различать, является ли входящее соединение TLS или SSH, а OpenVPN имеет функцию "share share", в которой он проксирует не-OpenVPN-подключения к другому порту.
Одним из возможных подходов является использование правил iptables, которые соответствуют строкам внутри пакетов. Первый пакет HTTP-запроса должен включать "HTTP/", тогда как пакет TLS ClientHello не будет. Затем соединение можно перенаправить на другой порт, который не использует TLS. Обратите внимание, что это потребует дополнительных накладных расходов из-за поиска строк во всех пакетах и довольно хакерского решения.
iptables --table nat --append PREROUTING --protocol tcp --dport 10433 --match string --string "HTTP/" --REDIRECT 1080