如何从一个端口为 Jetty 提供 https 和 http?
Posted
技术标签:
【中文标题】如何从一个端口为 Jetty 提供 https 和 http?【英文标题】:How do I serve https and http for Jetty from one port? 【发布时间】:2012-06-26 06:53:07 【问题描述】:(我知道这是一个重复的问题,但原发帖人问这个问题的原因是错误的。我并不是说我问这个问题是出于正确的原因,但让我们看看。)
我们有一个在非标准端口号上运行的 Web 服务。尽管用户似乎能够记住端口号,但有时他们会错误地键入 http: 而不是 https:。有人问我们是否可以在该端口上提供 HTTP,然后将它们重定向到同一端口上的 HTTPS。听起来很邪恶……我喜欢它的可用性,但感觉这应该是浏览器的工作?
我见过的一种解决方案是“在 Jetty 前编写自己的代理”。这个解决方案会起作用,但我认为它不会很好用,因为我不相信我可以编写一个与 Jetty 本身一样高效的代理。另外,即使代理本身很高效,所有数据仍然需要额外的一跳,这保证无论如何都会减慢流量。
还有比这更好的方法吗?也许 Jetty 本身有一些地方可以嵌入协议检测逻辑,这将允许利用它们的速度,同时还可以消除代理会引入的额外跃点。
【问题讨论】:
相关:***.com/q/6870637/552792 属于服务器故障,但无论如何答案是“不,你不能。” 嗯,我正在开发这个软件供其他人在他们的服务器上使用......但即便如此,serverfault 更合适?我确实在这样的时候想知道它,这个问题正处于两个网站相关性的边缘。 @bmargulies 不正确,可以这样做,见my answer。 从 jetty-9.4.15.v20190215 开始,Jetty 内置了对端口统一的支持;见this answer。 【参考方案1】:更新:有关如何将单个端口重定向到 HTTPS 和 HTTP 侦听器的说明,请参阅 this answer。如果出于某种原因您不使用该解决方案,请参见下文:
无法在同一端口上同时传输来自 http 和 https 的流量。 Jetty 使用两个完全不同的连接器来绑定到安全和非安全端口。事实上,我遇到的每台 Web 服务器都将这两个协议绑定到两个完全独立的端口。
出于可用性的考虑,我建议的一件事是使用默认端口,它对用户完全隐藏了端口。默认情况下 http 使用端口 80,默认情况下 https 使用端口 443。因此,如果您将连接器配置为分别在端口 80 和端口 443 上运行,那么您的用户不必键入端口,您的开发团队也不需要必须处理在 html、CSS、javascript 和其他资源的绝对路径中包含端口号。
Jetty 被设计成一个独立的 Web 服务器,与 Tomcat 的旧版本 不同,Apache 建议在 Apache HTTP 服务器后面运行它。因此,只要您没有其他 HTTP 服务器在运行,并且使用了这些端口,您就可以将 Jetty 配置为在默认端口上运行而不会出现任何问题。 这来自经验。我们正是以这种方式运行 Jetty。
最后,一个协议可以绑定到多个端口。因此,如果您当前在端口 8080 和 8443 端口上运行 Jetty 用于 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 个连接器,唯一真正的区别是端口号。简而言之,您可以为每个连接器/协议配置多个端口,但不能为同一端口配置多个协议/连接器。
【讨论】:
Apache 多年来一直没有建议在 Apache HTTP 守护进程后面运行 Tomcat。有些人会,有些人不会。 @bmargulies - 有很多人使用旧版本的 Tomcat,并在其前面运行 Apache。因此,这些准则适用于这些情况。不幸的是,并非每个人都处于最前沿。但为了正确和彻底,我将编辑我的答案。 如果有人能提供反馈,说明为什么会被否决,那将会很有帮助。这就是我们在组织中运行 Jetty 的方式,多年来,它在生产中完美运行,经过反复试验和测试。 虽然我也可能建议你和我一起投票,将它迁移到它所属的 serverfault。 如果我是管理员,我肯定会这样做,但不幸的是,我们经常遇到甚至无法获得 SSL 证书的用户,更不用说将应用程序配置为使用默认端口以外的端口了。我们不使用端口 80 作为默认端口的原因是我们希望其他服务已经在服务器上并试图避开他们的服务(加上部署到 UNIX 的常见问题,您甚至没有权限如果您不是以 root 身份运行,则绑定到端口 80...)【参考方案2】:更新:从 jetty-9.4.15.v20190215 起,Jetty 内置了对端口统一的支持;见this answer。
是的,我们可以
这是可能的,我们已经做到了。这里的代码适用于 Jetty 8;我没有用 Jetty 9 测试过,但this answer 有类似的 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
,但可以检查和存储客户端消息的第一个字节。
一些细节
一个非常hacky的位。我们的Connector
必须覆盖的方法之一是customize(Endpoint,Request)
。如果我们最终得到一个基于 SSL 的Endpoint
,我们可以传递给我们的超类;否则超类将抛出ClassCastException
,但只有在传递给其超类和在Request
上设置方案之后。所以我们传递给超类,但是当我们看到异常时撤消设置方案。
我们还覆盖 isConfidential()
和 isIntegral()
以确保我们的 servlet 可以正确使用 HttpServletRequest.isSecure()
来确定使用的是 HTTP 还是 HTTPS。
尝试从客户端读取第一个字节可能会抛出 IOException
,但我们可能不得不在不期望 IOException
的地方尝试这样做,在这种情况下,我们会保留异常并抛出它稍后。
在 Java >= 7 和 Java 6 中扩展 SocketChannel
看起来不同。在后一种情况下,只需注释掉 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();
//
【讨论】:
有趣的解决方案。我不认为这是可能的。只是一个不错的开源解决方案的想法——您是否考虑过将所有这些包装到一个自定义连接器中?就像有人可以将某些东西嵌入到他们的 XML 配置中然后瞧? 这不可能了,TLS/NPN/ALPN/SPDY/HTTP2/etc,这种技术不再支持了。 @JoakimErdfelt,你能解释一下吗?如果我没记错的话,NPN 和 ALPN 只是 TLS 的扩展,所以它们不应该与给定连接是否使用 TLS 的问题相关...... 使用自定义连接器窥视传入连接的技术不再可行,主要是由于围绕 npn/alpn 引导类路径修改的架构原因。在 Java 9 中将 alpn 嵌入到 JVM 中可能是可能的,但现在还不行。 @JoakimErdfelt 我修改了代码,即使使用 Jetty 9,您仍然可以使用 suche.org:443 进行检查(如果您使用 telnet,因为现代浏览器会因为 HSTS 预加载而拒绝它)【参考方案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("%s@%x%s->%s", new Object[]this.getClass().getSimpleName(), Integer.valueOf(this.hashCode()), this.getProtocol(), this._nextProtocol);
【讨论】:
这太棒了!正是我想要的。谢谢 我在这段代码中看到的唯一问题是在 MyReadAheadEndpoint.readAhead() 方法中。对于 jetty-9.4.0.v20161208,endPoint.fill() 偶尔会返回 0。因此,一些非 SSL 请求在 MySslConnectionFactory.newConnection() 中被错误地识别为 SSL。作为一种解决方法,我在 readAhead() 方法中添加了一些逻辑,以等待来自客户端的新字节长达 15 秒。 从 jetty-9.4.15.v20190215 开始,Jetty 内置了对端口统一的支持;见this answer。【参考方案4】:您可以通过编写自定义 Jetty ConnectionFactory 来实现这一点。我建议从复制和修改 SslConnectionFactory 和 SslConnection 的代码开始。您需要检查连接的前几个字节(必要时进行缓冲)以查找 SSL 客户端 Hello。使用 SSLv2 Hello,您可以通过两个长度字节来识别它,然后是 0x01,然后是版本字节。 SSLv3 Hello 以 0x16 开头,后跟版本字节。版本字节序列将为 SSL 3.0 的 0x03 0x00、SSL 2.0 的 0x02 0x00、TLS 1.0 的 0x03 0x01、TLS 1.1 的 0x03 0x02、TLS 1.2 的 0x03 0x03。有效的 HTTP 流量不应以这些字节序列开头。 (This answer有更多细节。)如果是SSL,通过SSLEngine;如果没有,则直接将其传递给下一个协议连接器。
【讨论】:
【参考方案5】:从 jetty-9.4.15.v20190215 开始,Jetty 通过 OptionalSslConnectionFactory 类内置了对端口统一的支持。
这是一个示例类,它在运行时将启动一个服务器,该服务器在单个端口 8000 上进行侦听,并将响应 HTTP 或 HTTPS。 (这是基于 Jetty 示例代码,用于单独的 HTTP 和 HTTPS 连接器here。)
import java.io.*;
import javax.servlet.http.*;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
public class Jetty9PortUnification
public static void main(String[] args) throws Exception
// Use example keystore and keys from Jetty distribution
String keystorePath = "jetty-distribution/demo-base/etc/keystore";
File keystoreFile = new File(keystorePath);
if (!keystoreFile.exists())
throw new FileNotFoundException(keystoreFile.getAbsolutePath());
Server server = new Server();
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(8000);
SecureRequestCustomizer src = new SecureRequestCustomizer();
httpConfig.addCustomizer(src);
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath());
sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
ServerConnector portUnified = new ServerConnector(server,
new OptionalSslConnectionFactory(sslConnectionFactory, HttpVersion.HTTP_1_1.asString()),
sslConnectionFactory,
httpConnectionFactory);
portUnified.setPort(8000);
server.addConnector(portUnified);
server.setHandler(new AbstractHandler()
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
response.setContentType("text/plain");
response.getWriter().println("Hello");
baseRequest.setHandled(true);
);
server.start();
server.join();
要运行它,您需要 javax.servlet-api-3.1.0.jar
、jetty-server-9.4.15.v20190215.jar
、jetty-util-9.4.15.v20190215.jar
、jetty-http-9.4.15.v20190215.jar
和 jetty-io-9.4.15.v20190215.jar
。
【讨论】:
【参考方案6】:即使将 Jetty 排除在外,这也是不可能的,因为服务器必须检测传入连接是 HTTP 还是 SSL/TLS。 TLS 协议并非旨在支持这种用法,因此任何实现都将是一种 hack(我也找不到)。
确实存在 SSL-SSH multiplexer 可以区分传入连接是 TLS 还是 SSH,Open*** 具有“端口共享”功能,它可以将非 Open*** 连接代理到另一个端口。
一种可能的方法是使用 iptables 规则来匹配数据包内的字符串。 HTTP 请求的第一个数据包应该包含“HTTP/”,而 TLS ClientHello 数据包不会。然后可以将连接重定向到不使用 TLS 的不同端口。请注意,由于在整个数据包中进行字符串搜索,这会产生额外的开销,并且是一个非常老套的解决方案。
iptables --table nat --append PREROUTING --protocol tcp --dport 10433 --match string --string "HTTP/" --REDIRECT 1080
【讨论】:
任何使用 iptables 的解决方案都不是真正可用的,因为我们部署在其他平台上,而且我们的用户群还没有达到他们可以重新配置防火墙的水平。 @Trejkaz 很公平;所以把这个想法应用到不同的防火墙上。正如我所说,这不是一个很好的解决方案,但它似乎是唯一的解决方案。 我不知道为什么这个问题上的每个人似乎都认为我是系统管理员,他在我的服务器上配置它以供用户使用......但我正在编写应该开箱即用。我们选择了一个默认端口,希望它不会与其他任何人的端口发生冲突,我只是想防御人们在测试中注意到的一些东西。如果我说“您可以通过如下配置来防止自己将 https: 错误输入为 http:...”,那么没有人会按照这些说明进行操作。 @Trejkaz 因为没有神奇的 Jetty 配置可以做到这一点,所以人们正试图提出下一个最佳解决方案来解决您的问题。如果你想让这个解决方案,那么你将不得不研究 TLS 协议并实现你自己的网络服务器或多路复用器服务。 1) 因为有工作证明这是可能的。事实证明你的答案是错误的。 2) 由于 SSL 协议的第一个字节不是 'A'-'Z' 之间的字符,因此可以区分。 3) 如果第一个字符是 A-Z,则可以创建一个除了传输字节之外什么都不做的 SSL 引擎,否则委托给真正的 SSL 引擎并将此模式宣布为 Ciphersuite 的 PLAIN_TEXT。所以它可以在客户端被检测到一个标记为不安全的。以上是关于如何从一个端口为 Jetty 提供 https 和 http?的主要内容,如果未能解决你的问题,请参考以下文章