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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/11182192/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-31 04:06:22  来源:igfitidea点击:

How do I serve https and http for Jetty from one port?

javahttphttpsjettyport

提问by Trejkaz

(I know it's a duplicate question but the original poster asked it for the wrong reason. I'm not implying that I'm asking it for the rightreason, but let's see.)

(我知道这是一个重复的问题,但原始海报问它的原因是错误的。我并不是暗示我问它的原因是正确的,但让我们看看。)

We have a web service which runs on a non-standard port number. Even though users seem to be able to remember the port number, occasionally they type http: instead of https: by mistake. Someone is asking whether we can serve HTTP on that port and then redirect them to HTTPS on the same port. It sounds evil... I like the usability but it feels like maybe it should be the browser's job to do this?

我们有一个在非标准端口号上运行的 Web 服务。尽管用户似乎能够记住端口号,但有时他们会错误地键入 http: 而不是 https:。有人问我们是否可以在该端口上提供 HTTP 服务,然后将它们重定向到同一端口上的 HTTPS。这听起来很邪恶......我喜欢它的可用性,但感觉这也许应该是浏览器的工作?

The one solution I have seen was "write your own proxy in front of Jetty." This solution would work, but I don't think it would work well as I am not confident that I can write a proxy which is as efficient as Jetty itself. Plus, even if the proxy itself is efficient, all the data would still have to go an additional hop, which is guaranteed to slow down the traffic anyway.

我见过的一种解决方案是“在 Jetty 面前编写自己的代理”。这个解决方案会起作用,但我认为它不会很好用,因为我不相信我可以编写一个与 Jetty 本身一样高效的代理。另外,即使代理本身是有效的,所有数据仍然需要多走一跳,这无论如何都会保证减慢流量。

Is there a better way than this? Perhaps Jetty itself has some place where the protocol detection logic could be wedged which would allow taking advantage of their speed while also removing the additional hop a proxy would introduce.

还有比这更好的方法吗?也许 Jetty 本身有一些地方可以楔入协议检测逻辑,这将允许利用它们的速度,同时消除代理可能引入的额外跃点。

回答by jmort253

Update: See this answerfor instructions on how to redirect a single port to both an HTTPS and HTTP listener. If for whatever reason you don't use that solution, see below:

更新:有关如何将单个端口重定向到 HTTPS 和 HTTP 侦听器的说明,请参阅此答案。如果出于某种原因您不使用该解决方案,请参见下文:

It isn't possible to pipe traffic from both http and https on the same port. Jetty uses two completely different connectors to bind to the secure and unsecure ports. In fact, every web server I've encountered binds the two protocols to two completely separate ports.

无法在同一端口上通过管道传输来自 http 和 https 的流量。Jetty 使用两个完全不同的连接器来绑定到安全和不安全端口。事实上,我遇到的每个 Web 服务器都将这两种协议绑定到两个完全独立的端口。

One thing I would suggest for usability's sake is to use default ports, which completely hides the port from the user. By default http uses port 80, and by default https uses port 443. So if you configure your connectors to run on port 80 and port 443 respectively, then your users don't have to type a port, and your development team doesn't have to handle including port numbers in absolute paths in HTML, CSS, JavaScript, and other resources.

为了可用性,我建议的一件事是使用默认端口,这对用户完全隐藏了端口。默认情况下,http 使用端口 80,默认情况下 https 使用端口 443。因此,如果您将连接器配置为分别在端口 80 和端口 443 上运行,那么您的用户不必键入端口,您的开发团队也不必必须处理在 HTML、CSS、JavaScript 和其他资源的绝对路径中包含端口号。

Jetty is designed to be a standalone Web server, unlike older versions of Tomcat, which Apache suggests run behind the Apache HTTP server. Therefore, as long as you have no other HTTP server running, and using those ports so you cannot, you should be able to configure Jetty to run on the default ports without any problem. This comes from experience. We run Jetty precisely in this manner.

Jetty 被设计为独立的 Web 服务器,这与旧版本的 Tomcat 不同,Apache 建议后者在 Apache HTTP 服务器之后运行。因此,只要您没有其他 HTTP 服务器在运行,并且不能使用这些端口,您就应该能够将 Jetty 配置为在默认端口上运行而不会出现任何问题。这来自经验。我们正是以这种方式运行 Jetty。

Finally, a protocol can be bound to more than one port. Thus, if you're currently running Jetty on ports 8080 for http and 8443 for https, you can leave those connectors active and add two more connectors for port 80 and port 443. This enabled backwards compatibility for the part of your app that is still using the port numbers and gives you time to walk this forward.

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

For the 2nd and 4th connectors, the only real differences are the port numbers. In short, you can configure multiple ports per connector/protocol, but you cannot configure multiple protocols/connectors for the same port.

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

回答by Robert Tupelo-Schneck

Update: As of jetty-9.4.15.v20190215 support for port unification is built into Jetty; see this answer.

更新:从 jetty-9.4.15.v20190215 开始,Jetty 内置了对端口统一的支持;看到这个答案

Yes We Can

我们可以

This is possible and we have done it. The code here works with Jetty 8; I have not tested with Jetty 9, but this answerhas similar code for Jetty 9.

这是可能的,我们已经做到了。此处的代码适用于 Jetty 8;我没有用 Jetty 9 测试过,但是这个答案有类似的 Jetty 9 代码。

By the way this is called port unification, and it is has apparently long been supported in Glassfishusing Grizzly.

顺便说一下,这被称为端口统一,显然,使用Grizzly 的Glassfish长期以来一直支持它。

Outline

大纲

The basic idea is to produce an implementation of org.eclipse.jetty.server.Connectorwhich can look ahead at the first byte of the client's request. Luckily both HTTP and HTTPS have the client start the communication. For HTTPS (and TLS/SSL generally) the first byte will be 0x16(TLS), or >= 0x80(SSLv2). For HTTP the first byte will be good-old printable 7-bit ASCII. Now, depending on the first byte, the Connectorwill either produce an SSL connection or a plain connection.

基本思想是生成一个org.eclipse.jetty.server.Connector可以提前查看客户端请求的第一个字节的实现。幸运的是,HTTP 和 HTTPS 都让客户端开始通信。对于 HTTPS(以及通常的 TLS/SSL),第一个字节将是0x16(TLS) 或>= 0x80(SSLv2)。对于 HTTP,第一个字节将是古老的可打印 7 位 ASCII。现在,根据第一个字节,Connector将生成 SSL 连接或普通连接。

In the code here we take advantage of the fact that Jetty's SslSelectChannelConnectoritself extends SelectChannelConnector, and has a newPlainConnection()method (calling its superclass to produce a non-SSL connection) as well as a newConnection()method (to produce an SSL connection). So our new Connectorcan extend SslSelectChannelConnectorand delegate to one of those methods after observing the first byte from the client.

在这里的代码中,我们利用了 JettySslSelectChannelConnector本身 extends的事实SelectChannelConnector,并且有一个newPlainConnection()方法(调用它的超类来产生一个非 SSL 连接)和一个newConnection()方法(产生一个 SSL 连接)。因此,我们的 newConnector可以SslSelectChannelConnector在观察来自客户端的第一个字节后扩展并委托给其中一个方法。

Unfortunately, we will be expected to create an instance of AsyncConnectionbeforethe first byte is available. Some methods of that instance may even be called before the first byte is available. So we create a LazyConnection implements AsyncConnectionwhich can figure out later which kind of connection it will delegate to, or even return sensible default responses to some methods before it knows.

不幸的是,我们需要AsyncConnection第一个字节可用之前创建一个实例。该实例的某些方法甚至可能在第一个字节可用之前被调用。因此,我们创建了一个LazyConnection implements AsyncConnection它可以在稍后确定它将委托给哪种连接,甚至在它知道之前返回对某些方法的合理默认响应。

Being NIO-based, our Connectorwill work with a SocketChannel. Luckily we can extend SocketChannelto create a ReadAheadSocketChannelWrapperwhich delegates to the "real" SocketChannelbut can inspect and store the first bytes of the client's message.

基于 NIO,我们Connector将使用SocketChannel. 幸运的是,我们可以扩展SocketChannel创建一个ReadAheadSocketChannelWrapper委托给“真实”SocketChannel但可以检查和存储客户端消息的第一个字节的 a 。

Some Details

一些细节

One very hacky bit. One of the methods our Connectormust override is customize(Endpoint,Request). If we end up with an SSL-based Endpointwe can just pass to our superclass; otherwise the superclass will throw a ClassCastException, but only after both passing to itssuperclass andsetting the scheme on the Request. So we pass to the superclass, but undo setting the scheme when we see the exception.

一个非常hacky的一点。我们Connector必须重写的方法之一是customize(Endpoint,Request). 如果我们最终得到一个基于 SSL 的,Endpoint我们就可以传递给我们的超类;否则超类将抛出ClassCastException,但只有在传递给它的超类Request. 所以我们传递给超类,但是当我们看到异常时撤消设置方案。

We also override isConfidential()and isIntegral()to ensure that our servlets can properly use HttpServletRequest.isSecure()to figure out whether HTTP or HTTPS was used.

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

Attempting to read the first byte from the client may throw an IOException, but we may have to attempt that in a place where an IOExceptionisn't expected, in which case we keep the exception around and throw it later.

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

Extending SocketChannellooks different in Java >= 7 and Java 6. In the latter case, just comment out the methods that Java 6 SocketChanneldoesn't have.

SocketChannel在 Java >= 7 和 Java 6 中扩展看起来不同。在后一种情况下,只需注释掉 Java 6SocketChannel没有的方法。

The Code

代码

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

回答by SkateScout

Based on the Answer "Yes We Can" i build the code that works with current jetty 9.3.11 and i think some would be interested.

基于答案“Yes We Can”,我构建了适用于当前码头 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});
}

}

}

回答by Simon Kissane

You could implement this by writing a custom Jetty ConnectionFactory. I would suggest starting by copying and modifying the code of SslConnectionFactory and SslConnection. You need to inspect the first few bytes of the connection (buffering as necessary) to look for an SSL Client Hello. With an SSLv2 Hello, you can identify that by two length bytes, followed by 0x01, followed by the version bytes. An SSLv3 Hello starts with 0x16 followed by the version bytes. The version byte sequences will be 0x03 0x00 for SSL 3.0, 0x02 0x00 for SSL 2.0, 0x03 0x01 for TLS 1.0, 0x03 0x02 for TLS 1.1, 0x03 0x03 for TLS 1.2. Valid HTTP traffic should never begin with these byte sequences. (This answerhas more details.) If it is SSL, pass it through the SSLEngine; if not, pass it directly to the next protocol connector.

您可以通过编写自定义 Jetty ConnectionFactory 来实现这一点。我建议从复制和修改 SslConnectionFactory 和 SslConnection 的代码开始。您需要检查连接的前几个字节(根据需要进行缓冲)以查找 SSL Client 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 流量不应以这些字节序列开始。(这个答案有更多细节。)如果是SSL,则通过SSLEngine;如果没有,则直接将其传递给下一个协议连接器。

回答by Robert Tupelo-Schneck

As of jetty-9.4.15.v20190215 support for port unification is built into Jetty, via the class OptionalSslConnectionFactory.

从 jetty-9.4.15.v20190215 开始,Jetty 通过OptionalSslConnectionFactory类内置了对端口统一的支持。

Here's an example class which, when run, will start up a server that listens on a single port 8000 and will respond to either HTTP or HTTPS. (This is based on the Jetty example code for separate HTTP and HTTPS connectors here.)

这是一个示例类,它在运行时将启动一个服务器,该服务器侦听单个端口 8000 并响应 HTTP 或 HTTPS。(这是基于此处用于单独 HTTP 和 HTTPS 连接器的 Jetty 示例代码。)

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

To run it you'll need 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, and jetty-io-9.4.15.v20190215.jar.

要运行它,你会需要javax.servlet-api-3.1.0.jarjetty-server-9.4.15.v20190215.jarjetty-util-9.4.15.v20190215.jarjetty-http-9.4.15.v20190215.jar,和jetty-io-9.4.15.v20190215.jar

回答by mgorven

Even taking Jetty out the picture, this isn't really possible, because the server would have to detect whether the incoming connection is HTTP or SSL/TLS. The TLS protocol isn't designed to support this usage, and so any implementation would be a hack (and I can't find any either).

即使把 Jetty 拿出来,这也不太可能,因为服务器必须检测传入的连接是 HTTP 还是 SSL/TLS。TLS 协议并非旨在支持这种用法,因此任何实现都将是一种黑客攻击(我也找不到任何实现)。

There does exist an SSL-SSH multiplexerwhich can distinguish whether an incoming connection is TLS or SSH, and OpenVPNhas a "port share" feature where it proxies non-OpenVPN connections to another port.

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

One possible approach is to use iptables rules which match strings inside the packets. The first packet of an HTTP request should include "HTTP/", whereas a TLS ClientHello packet would not. The connection could then be redirected to a different port which doesn't use TLS. Note that this would incur additional overhead due to string searches in entire packets, and is quite a hacky solution.

一种可能的方法是使用 iptables 规则来匹配数据包内的字符串。HTTP 请求的第一个数据包应包含“HTTP/”,而 TLS ClientHello 数据包则不会。然后可以将连接重定向到不使用 TLS 的其他端口。请注意,由于在整个数据包中进行字符串搜索,这会产生额外的开销,并且是一个非常棘手的解决方案。

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