17

(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 right reason, 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?

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.

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.

Hakanai
  • 12,010
  • 10
  • 62
  • 132

6 Answers6

15

Update: See this answer for 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:

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.

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.

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.

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.

<!-- 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.

Community
  • 1
  • 1
jamesmortensen
  • 33,636
  • 11
  • 99
  • 120
  • 1
    Apache hasn't suggested running Tomcat behind Apache HTTP Daemon for years. Some people do, and some people don't. – bmargulies Jun 25 '12 at 01:20
  • @bmargulies - There are lots of people using an older version of Tomcat, and running Apache in front of it. Thus, those guidelines apply in those scenarios. Not everyone is on the cutting edge, unfortunately. But in the interests of being **correct** and **thorough**, I'll edit my answer. – jamesmortensen Jun 25 '12 at 01:25
  • It would be helpful if someone could provide feedback on why this was downvoted. This is how we run Jetty in our organization, and it has worked flawlessly, tried and tested in production, for us for many years. – jamesmortensen Jun 25 '12 at 01:31
  • I did give you feedback, and I've now retracted the downvote. – bmargulies Jun 25 '12 at 01:37
  • 1
    Though I might also suggest that you join me in voting to migrate it to serverfault where it belongs. – bmargulies Jun 25 '12 at 01:38
  • @bmargulies - Thanks! I didn't want to spout accusations. I am, as you can see, very open to feedback to improving my answers. ;) I was actually just asking in the SF chat to see if they want it, but since there's no response, and since it *does* seem like a SF candidate, I'll join you. – jamesmortensen Jun 25 '12 at 01:38
  • 1
    If I were the admin I would definitely do this, but unfortunately we regularly encounter users who can't even get themselves an SSL certificate, let alone configure an application to use a port other than the default. The reason we don't ship with port 80 as the default port is that we expect other services to already be on the server and are trying to dodge theirs (plus the usual issue of deploying to UNIX, where you don't even have permission to bind to port 80 if you're not running as root...) – Hakanai Jun 25 '12 at 06:01
  • Are these "users" using a web browser or are these users developers? If they're users using a browser, they shouldn't need an SSL certificate. If they're developers connecting to your web service, then they should be able to figure out how to setup an SSL certificate or understand that the data isn't secure, IMHO. As for putting Jetty behind a proxy, you could do this as you describe in your question using Apache mod_proxy, but you'd lose the ability to use comet and other Jetty real time features. It depends on your goals in that regard. – jamesmortensen Jun 25 '12 at 06:39
  • [cont'd] - As for speed issues, your client may need to implement caching techniques when possible. – jamesmortensen Jun 25 '12 at 06:41
  • Since we're writing the server software, we have two levels of user: the users setting up the server (but even these people are Windows users so I wouldn't put much faith in their ability to configure things as they haven't shown us any such aptitude in the past) and the user connecting to that server who are the ones prone to mistyping the address. – Hakanai Jun 25 '12 at 22:35
10

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

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 answer has similar code for Jetty 9.

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

Outline

The basic idea is to produce an implementation of org.eclipse.jetty.server.Connector which 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 Connector will either produce an SSL connection or a plain connection.

In the code here we take advantage of the fact that Jetty's SslSelectChannelConnector itself 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 Connector can extend SslSelectChannelConnector and delegate to one of those methods after observing the first byte from the client.

Unfortunately, we will be expected to create an instance of AsyncConnection before the 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 AsyncConnection which can figure out later which kind of connection it will delegate to, or even return sensible default responses to some methods before it knows.

Being NIO-based, our Connector will work with a SocketChannel. Luckily we can extend SocketChannel to create a ReadAheadSocketChannelWrapper which delegates to the "real" SocketChannel but can inspect and store the first bytes of the client's message.

Some Details

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

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.

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 IOException isn't expected, in which case we keep the exception around and throw it later.

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

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();
//        }
    }
}
Robert Tupelo-Schneck
  • 10,047
  • 4
  • 47
  • 58
  • Interesting solution. I didn't think it was possible. Just a thought for a nice open source solution -- have you considered wrapping all this up into a custom connector? Like something someone could just embed in their XML configuration and voila? – jamesmortensen Jul 22 '14 at 20:56
  • This isn't possible anymore, with TLS/NPN/ALPN/SPDY/HTTP2/etc, this sort of technique is no longer supported. – Joakim Erdfelt Oct 09 '14 at 17:41
  • @JoakimErdfelt, can you explain? NPN and ALPN are just extensions to TLS, if I'm not mistaken, so they shouldn't be relevant to the question of whether a given connection is using TLS or not... – Robert Tupelo-Schneck Oct 10 '14 at 02:27
  • The technique of peeking into the incoming connection with a custom Connector isn't possible anymore, mainly due to architectural reasons surrounding the npn/alpn bootclasspath modifications. It might be possible in Java 9 with alpn baked into the JVM, but not right now. – Joakim Erdfelt Oct 10 '14 at 03:09
  • @JoakimErdfelt Would you explain what change broke this? Or you saying that it never worked completely? – John Cashew Apr 21 '15 at 23:10
  • 1
    @JoakimErdfelt i modified the code an it is still possible even with Jetty 9 you can check it with http://suche.org:443/ (if you use telnet since modern browser deny it because of HSTS-Preloading) – SkateScout Mar 20 '17 at 17:21
5

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.

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

}

SkateScout
  • 815
  • 14
  • 24
  • This is great! Exactly what I was looking for. Thx – Peter Jan 14 '17 at 00:43
  • The only issue I've seen with this code is in the MyReadAheadEndpoint.readAhead() method. With jetty-9.4.0.v20161208, the endPoint.fill() occasionally returns a 0. As a result, some non-SSL requests were incorrectly identified as SSL in MySslConnectionFactory.newConnection(). As a workaround, I added some logic to the readAhead() method to wait up to 15 seconds for new bytes from the client. – Peter Feb 08 '17 at 19:57
  • As of jetty-9.4.15.v20190215 support for port unification is built into Jetty; see [this answer](https://stackoverflow.com/a/54915462/394431). – Robert Tupelo-Schneck Feb 27 '19 at 22:25
3

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 answer has more details.) If it is SSL, pass it through the SSLEngine; if not, pass it directly to the next protocol connector.

Community
  • 1
  • 1
Simon Kissane
  • 4,373
  • 3
  • 34
  • 59
3

As of jetty-9.4.15.v20190215 support for port unification is built into Jetty, via the class 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.)

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.

Robert Tupelo-Schneck
  • 10,047
  • 4
  • 47
  • 58
-2

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).

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

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 --table nat --append PREROUTING --protocol tcp --dport 10433 --match string --string "HTTP/" --REDIRECT 1080
mgorven
  • 328
  • 1
  • 10
  • 1
    Any solution using iptables isn't really usable since we deploy on other platforms and our user base isn't quite up to the level where they can reconfigure their firewall. – Hakanai Jun 25 '12 at 06:09
  • @Trejkaz Fair enough; so take the idea and apply it to a different firewall. As I said, it's not a great solution, but it seems like the only one. – mgorven Jun 25 '12 at 06:44
  • I don't know why everyone on this question seems to assume I'm a system administrator who's configuring this on my server for users to use... but I'm writing software which is supposed to work out of the box. We picked a default port which should hopefully not collide with anyone else's, I'm just trying to defend against something people noticed in testing. If I say "you can prevent yourself mistyping https: as http: by configuring this as follows...", nobody is going to follow those instructions. – Hakanai Jun 25 '12 at 06:55
  • 1
    @Trejkaz Because there is no magic Jetty configuration which does this, and so people are trying to come up with the next best solution to your problem. If you want this to work out the box then you're going to have to study the TLS protocol and implement your own webserver or multiplexer service. – mgorven Jun 25 '12 at 07:00
  • 1) Since there are working proof that it is possible. It is proven that your answer is incorrect. 2) Since the first byte of the SSL Protocol is no Character between 'A'-'Z' it is distinguishable. 3) It would be possible to create an SSL-Engine that do nothing than transfer the bytes if first char is A-Z else do delegate to real SSL Engine and anounce this mode as PLAIN_TEXT for Ciphersuite. So it is detectable in the client an marked as unsecure. – SkateScout Mar 20 '17 at 17:26