/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package io.undertow.server;

import io.undertow.UndertowMessages;
import io.undertow.UndertowOptions;
import io.undertow.server.protocol.http.HttpServerConnection;

import org.xnio.ChannelListener;
import org.xnio.IoUtils;
import org.xnio.Options;
import io.undertow.connector.PooledByteBuffer;
import org.xnio.SslClientAuthMode;
import org.xnio.channels.Channels;
import org.xnio.channels.SslChannel;
import org.xnio.channels.StreamSourceChannel;

import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.security.cert.X509Certificate;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.cert.Certificate;
import java.util.concurrent.TimeUnit;

SSL session information that is read directly from the SSL session of the XNIO connection
Author:Stuart Douglas
/** * SSL session information that is read directly from the SSL session of the * XNIO connection * * @author Stuart Douglas */
public class ConnectionSSLSessionInfo implements SSLSessionInfo { private static final SSLPeerUnverifiedException PEER_UNVERIFIED_EXCEPTION = new SSLPeerUnverifiedException(""); private static final RenegotiationRequiredException RENEGOTIATION_REQUIRED_EXCEPTION = new RenegotiationRequiredException(); private static final long MAX_RENEGOTIATION_WAIT = 30000; private final SslChannel channel; private final HttpServerConnection serverConnection; private SSLPeerUnverifiedException unverified; private RenegotiationRequiredException renegotiationRequiredException; public ConnectionSSLSessionInfo(SslChannel channel, HttpServerConnection serverConnection) { this.channel = channel; this.serverConnection = serverConnection; } @Override public byte[] getSessionId() { return channel.getSslSession().getId(); } @Override public String getCipherSuite() { return channel.getSslSession().getCipherSuite(); } @Override public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException, RenegotiationRequiredException { if(unverified != null) { throw unverified; } if(renegotiationRequiredException != null) { throw renegotiationRequiredException; } try { return channel.getSslSession().getPeerCertificates(); } catch (SSLPeerUnverifiedException e) { try { SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { renegotiationRequiredException = RENEGOTIATION_REQUIRED_EXCEPTION; throw renegotiationRequiredException; } } catch (IOException e1) { //ignore, will not actually happen } unverified = PEER_UNVERIFIED_EXCEPTION; throw unverified; } } @Override public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException, RenegotiationRequiredException { if(unverified != null) { throw unverified; } if(renegotiationRequiredException != null) { throw renegotiationRequiredException; } try { return channel.getSslSession().getPeerCertificateChain(); } catch (SSLPeerUnverifiedException e) { try { SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { renegotiationRequiredException = RENEGOTIATION_REQUIRED_EXCEPTION; throw renegotiationRequiredException; } } catch (IOException e1) { //ignore, will not actually happen } unverified = PEER_UNVERIFIED_EXCEPTION; throw unverified; } } @Override public void renegotiate(HttpServerExchange exchange, SslClientAuthMode sslClientAuthMode) throws IOException { if ("TLSv1.3".equals(channel.getSslSession().getProtocol())) { // UNDERTOW-1812: TLSv1.3 does not support renegotiation, attempting renegotiation will block // the full MAX_RENEGOTIATION_WAIT timeout. throw UndertowMessages.MESSAGES.renegotiationNotSupported(); } unverified = null; renegotiationRequiredException = null; if (exchange.isRequestComplete()) { renegotiateNoRequest(exchange, sslClientAuthMode); } else { renegotiateBufferRequest(exchange, sslClientAuthMode); } } @Override public SSLSession getSSLSession() { return channel.getSslSession(); } public void renegotiateBufferRequest(HttpServerExchange exchange, SslClientAuthMode newAuthMode) throws IOException { int maxSize = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_BUFFERED_REQUEST_SIZE, UndertowOptions.DEFAULT_MAX_BUFFERED_REQUEST_SIZE); if (maxSize <= 0) { throw new SSLPeerUnverifiedException(""); } //first we need to read the request boolean requestResetRequired = false; StreamSourceChannel requestChannel = Connectors.getExistingRequestChannel(exchange); if (requestChannel == null) { requestChannel = exchange.getRequestChannel(); requestResetRequired = true; } PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().allocate(); boolean free = true; //if the pooled buffer should be freed int usedBuffers = 0; PooledByteBuffer[] poolArray = null; final int bufferSize = pooled.getBuffer().remaining(); int allowedBuffers = ((maxSize + bufferSize - 1) / bufferSize); poolArray = new PooledByteBuffer[allowedBuffers]; poolArray[usedBuffers++] = pooled; boolean overflow = false; try { int res; for(;;) { final ByteBuffer buf = pooled.getBuffer(); res = Channels.readBlocking(requestChannel, buf); if (!buf.hasRemaining()) { buf.flip(); if(allowedBuffers == usedBuffers) { overflow = true; break; } else { pooled = exchange.getConnection().getByteBufferPool().allocate(); poolArray[usedBuffers++] = pooled; } } else if(res == -1) { buf.flip(); break; } } free = false; Connectors.ungetRequestBytes(exchange, poolArray); if(overflow) { throw new SSLPeerUnverifiedException("Cannot renegotiate"); } renegotiateNoRequest(exchange, newAuthMode); } finally { if (free) { for(PooledByteBuffer buf : poolArray) { if(buf != null) { buf.close(); } } } if(requestResetRequired) { exchange.requestChannel = null; } } } public void renegotiateNoRequest(HttpServerExchange exchange, SslClientAuthMode newAuthMode) throws IOException { AbstractServerConnection.ConduitState oldState = serverConnection.resetChannel(); try { SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { SslHandshakeWaiter waiter = new SslHandshakeWaiter(); channel.getHandshakeSetter().set(waiter); //we use requested, to place nicely with other auth modes channel.setOption(Options.SSL_CLIENT_AUTH_MODE, newAuthMode); channel.getSslSession().invalidate(); channel.startHandshake(); serverConnection.getOriginalSinkConduit().flush(); ByteBuffer buff = ByteBuffer.wrap(new byte[1]); long end = System.currentTimeMillis() + MAX_RENEGOTIATION_WAIT; while (!waiter.isDone() && serverConnection.isOpen() && System.currentTimeMillis() < end) { int read = serverConnection.getSourceChannel().read(buff); if (read != 0) { throw new SSLPeerUnverifiedException(""); } if (!waiter.isDone()) { serverConnection.getSourceChannel().awaitReadable(end - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } } if(!waiter.isDone()) { if(serverConnection.isOpen()) { IoUtils.safeClose(serverConnection); throw UndertowMessages.MESSAGES.rengotiationTimedOut(); } else { IoUtils.safeClose(serverConnection); throw UndertowMessages.MESSAGES.rengotiationFailed(); } } } } finally { if (oldState != null) { serverConnection.restoreChannel(oldState); } } } private static class SslHandshakeWaiter implements ChannelListener<SslChannel> { private volatile boolean done = false; boolean isDone() { return done; } @Override public void handleEvent(SslChannel channel) { done = true; } } }