如何在一个端口上为Jetty提供https和http服务?

17

我知道这是一个重复的问题,但原作者提出了错误的原因。我并不意味着我提出问题的理由是正确的,但我们来看看吧。

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

我看到的一个解决方案是“在Jetty前编写自己的代理”。这个解决方案可能有效,但我不认为它会像Jetty本身一样高效。此外,即使代理本身是高效的,所有数据仍然必须经过额外的一跳,这肯定会拖慢流量。

是否有比这更好的方法?也许Jetty本身有一些地方可以插入协议检测逻辑,从而利用它们的速度,同时消除代理引入的额外跳数。


相关链接:https://dev59.com/81nUa4cB1Zd3GeqPdLrX - jamesmortensen
3
应该属于Serverfault范畴,无论如何答案都是“不行”。 - bmargulies
2
嗯,我正在开发这个软件让别人在他们的服务器上使用...但即便如此,Serverfault更加合适吗?像这样的时候我确实会思考,这个问题正处于两个网站相关性的边缘。 - Hakanai
@bmargulies错了,这是可以做到的,请参见我的回答 - Robert Tupelo-Schneck
1
从jetty-9.4.15.v20190215开始,端口统一的支持已经内置于Jetty中;请参见此答案 - Robert Tupelo-Schneck
6个回答

15

更新:请查看此答案,了解如何将单个端口重定向到HTTPS和HTTP监听器。如果由于某种原因您不使用该解决方案,请参见下文:

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

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

较旧版本的Tomcat不同,Jetty被设计为一个独立的Web服务器,Apache建议其在Apache HTTP服务器后面运行。因此,只要您没有其他HTTP服务器在运行,并且使用那些端口,您应该能够将Jetty配置为在默认端口上运行而没有任何问题。这是根据经验得出的结论。我们正是以这种方式运行Jetty。

最后,一个协议可以绑定到多个端口。因此,如果您当前在端口8080上运行Jetty进行http和在端口8443上运行Jetty进行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>

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


1
Apache多年来没有建议在Apache HTTP守护进程后面运行Tomcat。有些人这样做,有些人则不这样做。 - bmargulies
@bmargulies - 有很多人使用旧版本的Tomcat,并在其前面运行Apache。因此,这些准则适用于这些情况。不幸的是,并非每个人都处于最前沿。但为了做到“正确”和“全面”,我会编辑我的答案。 - jamesmortensen
如果有人可以提供反馈,解释一下为什么这个贴子会被踩就好了。这是我们在组织中运行Jetty的方式,对我们来说已经在生产环境中尝试并测试多年,表现非常出色,没有任何问题。 - jamesmortensen
1
虽然我也建议你和我一起投票将它迁移到它所属的serverfault上。 - bmargulies
1
如果我是管理员,我一定会这样做,但不幸的是我们经常遇到那些连SSL证书都无法获取,更别说配置应用程序使用非默认端口的用户。我们不将80端口作为默认端口出货的原因是因为我们预计服务器上已有其他服务,并且正在尝试规避它们(加上部署到UNIX的常见问题,如果您不以root身份运行,则甚至没有权限绑定到80端口...) - Hakanai
显示剩余5条评论

10

更新:从jetty-9.4.15.v20190215开始,对于端口统一支持已内置于Jetty中;请参考此答案

是的,我们可以

这是可能的,而且我们已经做到了。这里的代码适用于Jetty 8;我尚未测试过Jetty 9,但此答案包含了适用于Jetty 9的类似代码。

顺便说一下,这被称为端口统一,在使用GlassfishGrizzly的情况下,它显然已经被支持。

概述

基本思路是创建一个实现了org.eclipse.jetty.server.Connector接口的处理器,该处理器可以预先查看客户端请求的第一个字节。幸运的是,HTTP和HTTPS都要求客户端启动通信。对于HTTPS(以及TLS/SSL一般情况),第一个字节将是0x16(TLS)或>= 0x80(SSLv2)。对于HTTP,第一个字节将是良好的可打印7位ASCII码。现在,根据第一个字节,处理器将生成SSL连接或普通连接。

在这里的代码中,我们利用了Jetty的SslSelectChannelConnector本身扩展了SelectChannelConnector并具有newPlainConnection()方法(调用其超类以生成非SSL连接)和newConnection()方法(生成SSL连接)。因此,我们新的Connector可以扩展SslSelectChannelConnector并在观察到客户端的第一个字节后委托给其中的一个方法。

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

由于基于NIO,所以我们的Connector将使用SocketChannel工作。幸运的是,我们可以扩展SocketChannel以创建一个ReadAheadSocketChannelWrapper,该封装器委托给“真实”的SocketChannel,但可以检查并存储客户端消息的前几个字节。

一些细节

有一个非常巧妙的部分。我们的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配置中,然后就完成了? - jamesmortensen
这种技术已经不再可行了,因为TLS/NPN/ALPN/SPDY/HTTP2等协议已经不再支持这种技术。 - Joakim Erdfelt
@JoakimErdfelt,你能解释一下吗?NPN和ALPN只是TLS的扩展,如果我没记错的话,它们与一个连接是否使用TLS无关... - Robert Tupelo-Schneck
使用自定义连接器进行查看传入连接的技术不再可能,主要是由于围绕npn / alpn引导类路径修改的架构原因。可能在Java 9中使用alpn烘焙到JVM中,但现在不行。 - Joakim Erdfelt
@JoakimErdfelt,您能解释一下是哪个更改导致了这个问题吗?还是说它从来没有完全工作过? - John Cashew
1
@JoakimErdfelt,我修改了代码,即使使用Jetty 9仍然可以实现,您可以通过http://suche.org:443/进行检查(如果您使用telnet,因为现代浏览器由于HSTS-Preloading而拒绝它)。 - SkateScout

5

基于“Yes We Can”的回答,我编写了可以与当前Jetty 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});
}

}


太棒了!正是我寻找的。谢谢。 - Peter
我所看到的这段代码唯一的问题在于MyReadAheadEndpoint.readAhead()方法。使用jetty-9.4.0.v20161208时,endPoint.fill()偶尔会返回0。因此,一些非SSL请求被错误地识别为SSL请求,在MySslConnectionFactory.newConnection()中。为了解决这个问题,我在readAhead()方法中添加了一些逻辑,等待客户端发送新字节的时间最长为15秒。 - Peter
自从jetty-9.4.15.v20190215版本,端口统一支持已经内置于Jetty中;请参阅这个答案 - Robert Tupelo-Schneck

3

您可以通过编写自定义的Jetty ConnectionFactory来实现此功能。建议您先复制并修改SslConnectionFactory和SslConnection的代码。您需要检查连接的前几个字节(必要时进行缓冲),以查找SSL客户端Hello。对于SSLv2 Hello,您可以通过两个长度字节标识,然后是0x01,然后是版本字节。SSLv3 Hello以0x16开头,后跟版本字节。版本字节序列为0x03 0x00表示SSL 3.0,0x02 0x00表示SSL 2.0,0x03 0x01表示TLS 1.0,0x03 0x02表示TLS 1.1,0x03 0x03表示TLS 1.2。有效的HTTP数据流不应以这些字节序列开头。 (该答案有更多细节.) 如果它是SSL,则将其通过SSLEngine传递;如果不是,则直接传递到下一个协议连接器。


3

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

以下是一个示例类,当运行此类时,将启动一个服务器,侦听单个端口8000,并将响应HTTP或HTTPS请求。 (基于Jetty的分离HTTP和HTTPS连接器的示例代码.)

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.jarjetty-server-9.4.15.v20190215.jarjetty-util-9.4.15.v20190215.jarjetty-http-9.4.15.v20190215.jarjetty-io-9.4.15.v20190215.jar


-2
即使将Jetty从图片中移除,这也是不可能的,因为服务器必须检测传入连接是HTTP还是SSL/TLS。TLS协议并不支持此用法,因此任何实现都将是一个hack(我也找不到任何实现)。
存在一种SSL-SSH多路复用器,可以区分传入连接是TLS还是SSH,而OpenVPN具有“端口共享”功能,可以将非OpenVPN连接代理到另一个端口。
一种可能的方法是使用iptables规则匹配数据包中的字符串。 HTTP请求的第一个数据包应包含“HTTP /”,而TLS ClientHello数据包则不应包含该字符串。然后可以将连接重定向到不使用TLS的不同端口。请注意,这将由于在整个数据包中进行字符串搜索而产生额外的开销,并且是相当hacky的解决方案。
iptables --table nat --append PREROUTING --protocol tcp --dport 10433 --match string --string "HTTP/" --REDIRECT 1080

1
任何使用iptables的解决方案都不是很实用,因为我们部署在其他平台上,而我们的用户群体还没有达到可以重新配置其防火墙的水平。 - Hakanai
@Trejkaz 好的,那就把这个想法应用到另一个防火墙上。正如我所说,这不是一个很好的解决方案,但似乎是唯一的选择。 - mgorven
我不知道为什么这个问题上的每个人似乎都认为我是一个系统管理员,正在为用户配置我的服务器使用...但我正在编写的软件应该可以直接使用。我们选择了一个默认端口,希望不会与其他人发生冲突,我只是试图防止测试中出现的一些问题。如果我说“您可以通过以下配置来防止自己将https:误写为http:...”,没有人会遵循这些说明。 - Hakanai
1
@Trejkaz 因为没有魔法Jetty配置可以做到这一点,所以人们正在尝试想出下一个最好的解决方案来解决你的问题。如果你想让它开箱即用,那么你就必须学习TLS协议并实现自己的Web服务器或多路复用服务。 - mgorven
  1. 既然有工作证明表明这是可能的,那么你的答案是错误的已经被证实了。
  2. 由于 SSL 协议的第一个字节不在 'A'-'Z' 字符之间,因此它是可区分的。
  3. 可以创建一个 SSL 引擎,仅在第一个字符为 A-Z 时传输字节,否则委托给真正的 SSL 引擎,并将此模式声明为 PLAIN_TEXT 用于密码套件。因此,在客户端中可以检测到它并标记为不安全。
- SkateScout

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接