如何从一个端口为码头提供https和http服务?是的,我们能

2022-09-03 00:11:47

(我知道这是一个重复的问题,但原来的海报问它的原因是错误的。我并不是说我问这个问题的理由是正确的,但让我们看看。

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

我看到的一个解决方案是“在码头前写你自己的代理”。这个解决方案会起作用,但我认为它不会很好地工作,因为我不相信我可以编写一个与Jetty本身一样有效的代理。另外,即使代理本身是有效的,所有数据仍然必须经过一个额外的跃点,这肯定会减慢流量。

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


答案 1

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

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

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

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

最后,一个协议可以绑定到多个端口。因此,如果您当前在 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>

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


答案 2

更新:从码头开始-9.4.15.v20190215对端口统一的支持内置于码头;看到这个答案

是的,我们能

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

顺便说一句,这被称为港口统一,显然长期以来一直使用灰熊玻璃鱼支持。

大纲

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

在这里的代码中,我们利用了 Jetty 本身扩展的事实,并且有一个方法(调用其超类以生成非 SSL 连接)以及一个方法(用于生成 SSL 连接)。因此,我们的 new 可以在观察来自客户端的第一个字节后扩展并委派给这些方法之一。SslSelectChannelConnectorSelectChannelConnectornewPlainConnection()newConnection()ConnectorSslSelectChannelConnector

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

作为基于NIO的,我们将与.幸运的是,我们可以扩展以创建一个将哪个委托给“真实”,但可以检查和存储客户端消息的第一个字节。ConnectorSocketChannelSocketChannelReadAheadSocketChannelWrapperSocketChannel

一些细节

一个非常笨拙的位。我们必须覆盖的方法之一是 。如果我们最终得到一个基于SSL的,我们可以传递给我们的超类;否则,超类将抛出一个 ,但只有在传递给它的超类并在 上设置方案之后。因此,我们传递给超类,但在看到异常时撤消方案设置。Connectorcustomize(Endpoint,Request)EndpointClassCastExceptionRequest

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

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

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

守则

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

推荐