如何从一个端口为Jetty提供https和http?

(我知道这是一个重复的问题,但是原来的海报却错误地提出了这个问题,我并不是暗示我是在为了正确的理由而提出这个问题,但是让我们来看看)。

我们有一个运行在非标准端口号上的Web服务。 即使用户似乎能够记住端口号,偶尔也会错误地inputhttp:而不是https:。 有人问我们是否可以在该端口上提供HTTP,然后将它们redirect到同一端口上的HTTPS。 这听起来是邪恶的…我喜欢可用性,但它也许应该是浏览器的工作,这样做?

我看到的一个解决scheme是“在docker前写你自己的代理”。 这个解决scheme是可行的,但是我不认为它会工作得很好,因为我不确定我能写一个和Jetty本身一样高效的代理。 另外,即使代理本身是有效的,所有的数据仍然需要额外的一跳,这保证了stream量减慢。

有没有比这更好的方法? 也许Jetty本身有一个地方,协议检测逻辑可以被楔入,这将允许利用他们的速度,同时也删除代理将引入的额外跳跃。

更新:有关如何将单个端口redirect到HTTPS和HTTP侦听器的说明,请参阅此答案 。 如果由于某种原因您不使用该解决scheme,请参阅下面的内容:

无法在同一个端口上同时pipe理来自http和https的stream量。 Jetty使用两个完全不同的连接器绑定到安全和不安全的端口。 实际上,我遇到的每个Web服务器都将两个协议绑定到两个完全独立的端口。

有一件事我build议可用性的缘故是使用默认端口,从用户完全隐藏的端口。 默认情况下,http使用端口80,默认情况下,https使用端口443.因此,如果将连接器分别configuration为在端口80和端口443上运行,则用户不必input端口,开发团队也不必须在HTML,CSS,JavaScript和其他资源中处理绝对path中的端口号。

Jetty被devise成一个独立的Web服务器,不像旧版本的Tomcat ,Apachebuild议在Apache HTTP服务器后运行。 因此,只要你没有运行其他的HTTP服务器,并且使用这些端口,那么你应该能够configurationJetty在默认的端口上运行,没有任何问题。 这是来自经验。 我们正是以这种方式运行Jetty。

最后,协议可以绑定到多个端口。 因此,如果您当前在端口8080上运行Jetty,而在https上运行8443端口,则可以使这些连接器保持活动状态,并为端口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> 

对于第二和第四个连接器,唯一的区别是端口号。 简而言之,您可以为每个连接器/协议configuration多个端口,但不能为同一端口configuration多个协议/连接器。

我们可以

这是可能的,我们已经做到了。 这里的代码适用于Jetty 8; 我没有testing过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并在观察客户端的第一个字节后委托给其中一个方法。

不幸的是,我们希望第一个字节可用之前创build一个AsyncConnection的实例。 在第一个字节可用之前,甚至可以调用该实例的某些方法。 所以我们创build一个LazyConnection implements AsyncConnection ,它可以在稍后找出它将委托给哪种types的连接,甚至可以在它知道之前将某些方法返回合理的默认响应。

基于NIO,我们的Connector将与SocketChannel工作。 幸运的是,我们可以扩展SocketChannel来创build一个ReadAheadSocketChannelWrapper ,它委托给“真正的” SocketChannel但可以检查和存储客户端消息的第一个字节。

一些细节

一个非常hacky位。 我们的Connector必须覆盖的方法之一是customize(Endpoint,Request) 。 如果我们结束了一个基于SSL的Endpoint我们可以传给我们的超类; 否则超类将抛出一个ClassCastException ,但是只有在传递给它的超类Request上设置scheme之后。 所以我们传递给超类,但是当我们看到这个exception的时候撤消设置scheme。

我们还会覆盖isConfidential()isIntegral()以确保我们的servlet可以正确使用HttpServletRequest.isSecure()来确定是否使用了HTTP或HTTPS。

尝试从客户端读取第一个字节可能会抛出一个IOException ,但是我们可能不得不在IOException不被期望的地方尝试,在这种情况下,我们会保留exception并在稍后抛出exception。

扩展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(); // } } } 

基于答案“是的,我们可以”,我build立了与现在的docker9.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}); } 

}

你可以通过编写一个自定义的Jetty ConnectionFactory来实现这个。 我会build议通过复制和修改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。 有效的HTTPstream量不应该以这些字节序列开始。 ( 这个答案有更多细节。)如果是SSL,则通过SSLEngine传递; 如果不是,则直接将其传递给下一个协议连接器。

即使把Jetty拿出来,这也是不可能的,因为服务器将不得不检测传入的连接是HTTP还是SSL / TLS。 TLS协议不是为了支持这个用法而devise的,所以任何实现都是黑客(我也找不到)。

确实存在一个SSL-SSH多路复用器 ,可以区分传入连接是TLS还是SSH,而OpenVPN具有“端口共享”function,可以将非OpenVPN连接代理到另一个端口。

一种可能的方法是使用匹配包内的string的iptables规则。 HTTP请求的第一个数据包应该包含“HTTP /”,而TLS ClientHello数据包不会。 然后可以将连接redirect到不使用TLS的不同端口。 请注意,由于在整个数据包中进行stringsearch,这将导致额外的开销,这是一个相当麻烦的解决scheme。

 iptables --table nat --append PREROUTING --protocol tcp --dport 10433 --match string --string "HTTP/" --REDIRECT 1080