如何从一个端口为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