475927 - SecureRequestCustomizer fails to match host.

Fixed by storing in the SSLSession the SNI names correspondent to the
alias that was selected when the TLS connection was initiated.
This commit is contained in:
Simone Bordet 2015-08-26 16:03:17 +02:00
parent 874300472e
commit 7057dae67b
7 changed files with 386 additions and 205 deletions

View File

@ -18,11 +18,6 @@
package org.eclipse.jetty.io;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@ -43,7 +38,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.io.ClientConnectionFactory.Helper;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.OS;
import org.eclipse.jetty.util.BufferUtil;
@ -51,6 +45,11 @@ import org.eclipse.jetty.util.IO;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class IOTest
{
@Test
@ -437,9 +436,9 @@ public class IOTest
connector.bind(null);
InetSocketAddress addr=(InetSocketAddress)connector.getLocalAddress();
Future<AsynchronousSocketChannel> acceptor = connector.accept();
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
client.connect(new InetSocketAddress("127.0.0.1",addr.getPort())).get(5, TimeUnit.SECONDS);
AsynchronousSocketChannel server = acceptor.get(5, TimeUnit.SECONDS);
@ -457,14 +456,14 @@ public class IOTest
Assert.assertEquals(ByteBuffer.wrap(data), read);
}
@Test
public void testGatherWrite() throws Exception
{
File dir = MavenTestingUtils.getTargetTestingDir();
if (!dir.exists())
dir.mkdir();
File file = File.createTempFile("test",".txt",dir);
file.deleteOnExit();
FileChannel out = FileChannel.open(file.toPath(),
@ -472,7 +471,7 @@ public class IOTest
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.DELETE_ON_CLOSE);
ByteBuffer[] buffers = new ByteBuffer[4096];
long expected=0;
for (int i=0;i<buffers.length;i++)
@ -480,22 +479,12 @@ public class IOTest
buffers[i]=BufferUtil.toBuffer(i);
expected+=buffers[i].remaining();
}
long wrote = IO.write(out,buffers,0,buffers.length);
assertEquals(expected,wrote);
for (int i=0;i<buffers.length;i++)
assertEquals(0,buffers[i].remaining());
}
@Test
public void testDomain()
{
assertTrue(IO.isInDomain("foo.com","foo.com"));
assertTrue(IO.isInDomain("www.foo.com","foo.com"));
assertFalse(IO.isInDomain("foo.com","bar.com"));
assertFalse(IO.isInDomain("www.foo.com","bar.com"));
}
}

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.server;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
@ -29,40 +30,39 @@ import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/* ------------------------------------------------------------ */
/** Customizer that extracts the attribute from an {@link SSLContext}
/**
* <p>Customizer that extracts the attribute from an {@link SSLContext}
* and sets them on the request with {@link ServletRequest#setAttribute(String, Object)}
* according to Servlet Specification Requirements.
* according to Servlet Specification Requirements.</p>
*/
public class SecureRequestCustomizer implements HttpConfiguration.Customizer
{
private static final Logger LOG = Log.getLogger(SecureRequestCustomizer.class);
/**
* The name of the SSLSession attribute that will contain any cached information.
*/
public static final String CACHED_INFO_ATTR = CachedInfo.class.getName();
private boolean _sniHostCheck;
public SecureRequestCustomizer()
{
this(true);
}
public SecureRequestCustomizer(boolean sniHostCheck)
{
_sniHostCheck=sniHostCheck;
}
@Override
public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
{
@ -77,10 +77,9 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
}
}
/* ------------------------------------------------------------ */
/*
* Customise the request attributes to be set for SSL requests. <br>
* The requirements of the Servlet specs are:
/**
* <p>Customizes the request attributes to be set for SSL requests.</p>
* <p>The requirements of the Servlet specs are:</p>
* <ul>
* <li> an attribute named "javax.servlet.request.ssl_session_id" of type
* String (since Servlet Spec 3.0).</li>
@ -95,8 +94,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
* </li>
* </ul>
*
* @param request
* HttpRequest to be customised.
* @param request HttpRequest to be customized.
*/
public void customize(SSLEngine sslEngine, Request request)
{
@ -105,19 +103,27 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
if (_sniHostCheck)
{
String sniName = (String)sslSession.getValue("org.eclipse.jetty.util.ssl.sniname");
if (sniName!=null && !sniName.equalsIgnoreCase(request.getServerName()))
String name = request.getServerName();
@SuppressWarnings("unchecked")
List<String> hosts = (List<String>)sslSession.getValue(SniX509ExtendedKeyManager.SNI_HOSTS);
@SuppressWarnings("unchecked")
List<String> wilds = (List<String>)sslSession.getValue(SniX509ExtendedKeyManager.SNI_WILDS);
if (hosts != null && !hosts.stream().anyMatch(host -> host.equalsIgnoreCase(name)))
{
String wild=(String)sslSession.getValue("org.eclipse.jetty.util.ssl.sniwild");
String name=request.getServerName();
if (wild==null || !IO.isInDomain(name,wild))
// Browsers may reuse the same connection if they can prove that two domains
// have the same certificate and IP, for example domain.com and www.domain.com.
if (wilds == null || !wilds.stream().anyMatch(wild -> isDomainOrSubDomain(name, wild)))
{
LOG.warn("Host does not match SNI Name: {}/{}!={}",sniName,wild,request.getServerName());
LOG.warn("Host {} does not match SNI hosts: {}/{}",name,hosts,wilds);
throw new BadMessageException(400,"Host does not match SNI");
}
}
if (LOG.isDebugEnabled())
LOG.debug("Host {} matched SNI hosts: {}/{}",name,hosts,wilds);
}
try
{
String cipherSuite=sslSession.getCipherSuite();
@ -132,9 +138,9 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
certs=cachedInfo.getCerts();
idStr=cachedInfo.getIdStr();
}
else
else
{
keySize=new Integer(SslContextFactory.deduceKeyLength(cipherSuite));
keySize=SslContextFactory.deduceKeyLength(cipherSuite);
certs=SslContextFactory.getCertChain(sslSession);
byte[] bytes = sslSession.getId();
idStr = TypeUtil.toHexString(bytes);
@ -155,15 +161,21 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
}
}
public static boolean isDomainOrSubDomain(String name, String domain)
{
if (!name.endsWith(domain))
return false;
if (name.length() == domain.length())
return true;
return name.charAt(name.length() - domain.length() - 1) == '.';
}
@Override
public String toString()
{
return String.format("%s@%x",this.getClass().getSimpleName(),hashCode());
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/**
* Simple bundle of information that is cached in the SSLSession. Stores the
* effective keySize and the client certificate chain.
@ -196,7 +208,4 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
return _idStr;
}
}
}

View File

@ -0,0 +1,37 @@
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SecureRequestCustomizerTest
{
@Test
public void testDomain()
{
assertTrue(SecureRequestCustomizer.isDomainOrSubDomain("foo.com", "foo.com"));
assertTrue(SecureRequestCustomizer.isDomainOrSubDomain("www.foo.com", "foo.com"));
assertFalse(SecureRequestCustomizer.isDomainOrSubDomain("bad_foo.com", "foo.com"));
assertFalse(SecureRequestCustomizer.isDomainOrSubDomain("foo.com", "bar.com"));
assertFalse(SecureRequestCustomizer.isDomainOrSubDomain("www.foo.com","bar.com"));
}
}

View File

@ -21,10 +21,13 @@ package org.eclipse.jetty.server.ssl;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
@ -50,6 +53,7 @@ import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.ConcurrentArrayQueue;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
import org.junit.After;
@ -96,9 +100,10 @@ public class SniSslConnectionFactoryTest
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
response.setStatus(200);
response.getWriter().write("url=" + request.getRequestURI() + "\nhost=" + request.getServerName());
response.flushBuffer();
response.setHeader("X-URL", request.getRequestURI());
response.setHeader("X-HOST", request.getServerName());
}
});
@ -117,7 +122,7 @@ public class SniSslConnectionFactoryTest
public void testConnect() throws Exception
{
String response = getResponse("127.0.0.1", null);
Assert.assertThat(response, Matchers.containsString("host=127.0.0.1"));
Assert.assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1"));
}
@Test
@ -142,39 +147,39 @@ public class SniSslConnectionFactoryTest
// The first entry in the keystore is www.example.com, and it will
// be returned by default, so make sure that here we don't ask for it.
String response = getResponse("jetty.eclipse.org", "jetty.eclipse.org");
Assert.assertThat(response, Matchers.containsString("host=jetty.eclipse.org"));
Assert.assertThat(response, Matchers.containsString("X-HOST: jetty.eclipse.org"));
}
@Test
public void testSNIConnect() throws Exception
{
String response = getResponse("jetty.eclipse.org", "jetty.eclipse.org");
Assert.assertThat(response, Matchers.containsString("host=jetty.eclipse.org"));
Assert.assertThat(response, Matchers.containsString("X-HOST: jetty.eclipse.org"));
response = getResponse("www.example.com", "www.example.com");
Assert.assertThat(response, Matchers.containsString("host=www.example.com"));
Assert.assertThat(response, Matchers.containsString("X-HOST: www.example.com"));
response = getResponse("foo.domain.com", "*.domain.com");
Assert.assertThat(response, Matchers.containsString("host=foo.domain.com"));
Assert.assertThat(response, Matchers.containsString("X-HOST: foo.domain.com"));
response = getResponse("m.san.com", "san example");
Assert.assertThat(response, Matchers.containsString("host=m.san.com"));
Assert.assertThat(response, Matchers.containsString("X-HOST: m.san.com"));
response = getResponse("www.san.com", "san example");
Assert.assertThat(response, Matchers.containsString("host=www.san.com"));
Assert.assertThat(response, Matchers.containsString("X-HOST: www.san.com"));
}
@Test
public void testWildSNIConnect() throws Exception
{
String response = getResponse("domain.com", "www.domain.com", "*.domain.com");
Assert.assertThat(response, Matchers.containsString("host=www.domain.com"));
Assert.assertThat(response, Matchers.containsString("X-HOST: www.domain.com"));
response = getResponse("domain.com", "domain.com", "*.domain.com");
Assert.assertThat(response, Matchers.containsString("host=domain.com"));
Assert.assertThat(response, Matchers.containsString("X-HOST: domain.com"));
response = getResponse("www.domain.com", "www.domain.com", "*.domain.com");
Assert.assertThat(response, Matchers.containsString("host=www.domain.com"));
Assert.assertThat(response, Matchers.containsString("X-HOST: www.domain.com"));
}
@Test
@ -185,11 +190,138 @@ public class SniSslConnectionFactoryTest
Assert.assertThat(response, Matchers.containsString("Host does not match SNI"));
}
@Test
public void testSameConnectionRequestsForManyDomains() throws Exception
{
SslContextFactory clientContextFactory = new SslContextFactory(true);
clientContextFactory.start();
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
{
SNIHostName serverName = new SNIHostName("m.san.com");
SSLParameters params = sslSocket.getSSLParameters();
params.setServerNames(Collections.singletonList(serverName));
sslSocket.setSSLParameters(params);
sslSocket.startHandshake();
// The first request binds the socket to an alias.
String request = "" +
"GET /ctx/path HTTP/1.1\r\n" +
"Host: m.san.com\r\n" +
"\r\n";
OutputStream output = sslSocket.getOutputStream();
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
InputStream input = sslSocket.getInputStream();
String response = response(input);
Assert.assertTrue(response.startsWith("HTTP/1.1 200 "));
// Same socket, send a request for a different domain but same alias.
request = "" +
"GET /ctx/path HTTP/1.1\r\n" +
"Host: www.san.com\r\n" +
"\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
Assert.assertTrue(response.startsWith("HTTP/1.1 200 "));
// Same socket, send a request for a different domain but different alias.
request = "" +
"GET /ctx/path HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
Assert.assertTrue(response.startsWith("HTTP/1.1 400 "));
Assert.assertThat(response, Matchers.containsString("Host does not match SNI"));
}
finally
{
clientContextFactory.stop();
}
}
@Test
public void testSameConnectionRequestsForManyWildDomains() throws Exception
{
SslContextFactory clientContextFactory = new SslContextFactory(true);
clientContextFactory.start();
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
{
SNIHostName serverName = new SNIHostName("www.domain.com");
SSLParameters params = sslSocket.getSSLParameters();
params.setServerNames(Collections.singletonList(serverName));
sslSocket.setSSLParameters(params);
sslSocket.startHandshake();
String request = "" +
"GET /ctx/path HTTP/1.1\r\n" +
"Host: www.domain.com\r\n" +
"\r\n";
OutputStream output = sslSocket.getOutputStream();
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
InputStream input = sslSocket.getInputStream();
String response = response(input);
Assert.assertTrue(response.startsWith("HTTP/1.1 200 "));
// Now, on the same socket, send a request for a different valid domain.
request = "" +
"GET /ctx/path HTTP/1.1\r\n" +
"Host: assets.domain.com\r\n" +
"\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
Assert.assertTrue(response.startsWith("HTTP/1.1 200 "));
// Now make a request for an invalid domain for this connection.
request = "" +
"GET /ctx/path HTTP/1.1\r\n" +
"Host: www.example.com\r\n" +
"\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
response = response(input);
Assert.assertTrue(response.startsWith("HTTP/1.1 400 "));
Assert.assertThat(response, Matchers.containsString("Host does not match SNI"));
}
finally
{
clientContextFactory.stop();
}
}
private String response(InputStream input) throws IOException
{
Utf8StringBuilder buffer = new Utf8StringBuilder();
int crlfs = 0;
while (true)
{
int read = input.read();
Assert.assertTrue(read >= 0);
buffer.append((byte)read);
crlfs = (read == '\r' || read == '\n') ? crlfs + 1 : 0;
if (crlfs == 4)
break;
}
return buffer.toString();
}
private String getResponse(String host, String cn) throws Exception
{
String response = getResponse(host, host, cn);
Assert.assertThat(response, Matchers.startsWith("HTTP/1.1 200 OK"));
Assert.assertThat(response, Matchers.containsString("url=/ctx/path"));
Assert.assertThat(response, Matchers.startsWith("HTTP/1.1 200 "));
Assert.assertThat(response, Matchers.containsString("X-URL: /ctx/path"));
return response;
}
@ -198,33 +330,34 @@ public class SniSslConnectionFactoryTest
SslContextFactory clientContextFactory = new SslContextFactory(true);
clientContextFactory.start();
SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory();
SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port);
if (cn != null)
try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port))
{
SNIHostName serverName = new SNIHostName(sniHost);
List<SNIServerName> serverNames = new ArrayList<>();
serverNames.add(serverName);
if (cn != null)
{
SNIHostName serverName = new SNIHostName(sniHost);
List<SNIServerName> serverNames = new ArrayList<>();
serverNames.add(serverName);
SSLParameters params = sslSocket.getSSLParameters();
params.setServerNames(serverNames);
sslSocket.setSSLParameters(params);
SSLParameters params = sslSocket.getSSLParameters();
params.setServerNames(serverNames);
sslSocket.setSSLParameters(params);
}
sslSocket.startHandshake();
if (cn != null)
{
X509Certificate cert = ((X509Certificate)sslSocket.getSession().getPeerCertificates()[0]);
Assert.assertThat(cert.getSubjectX500Principal().getName("CANONICAL"), Matchers.startsWith("cn=" + cn));
}
String response = "GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _port + "\r\n\r\n";
sslSocket.getOutputStream().write(response.getBytes(StandardCharsets.ISO_8859_1));
return IO.toString(sslSocket.getInputStream());
}
sslSocket.startHandshake();
if (cn != null)
finally
{
X509Certificate cert = ((X509Certificate)sslSocket.getSession().getPeerCertificates()[0]);
Assert.assertThat(cert.getSubjectX500Principal().getName("CANONICAL"), Matchers.startsWith("cn=" + cn));
clientContextFactory.stop();
}
sslSocket.getOutputStream().write(("GET /ctx/path HTTP/1.0\r\nHost: " + reqHost + ":" + _port + "\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
String response = IO.toString(sslSocket.getInputStream());
sslSocket.close();
clientContextFactory.stop();
return response;
}
@Test
@ -260,7 +393,7 @@ public class SniSslConnectionFactoryTest
});
String response = getResponse("127.0.0.1", null);
Assert.assertThat(response, Matchers.containsString("host=127.0.0.1"));
Assert.assertThat(response, Matchers.containsString("X-HOST: 127.0.0.1"));
Assert.assertEquals("customize connector class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());
Assert.assertEquals("customize ssl class org.eclipse.jetty.io.ssl.SslConnection,false", history.poll());

View File

@ -43,10 +43,10 @@ import org.eclipse.jetty.util.log.Logger;
* Provides stream handling utilities in
* singleton Threadpool implementation accessed by static members.
*/
public class IO
public class IO
{
private static final Logger LOG = Log.getLogger(IO.class);
/* ------------------------------------------------------------------- */
public final static String
CRLF = "\015\012";
@ -80,9 +80,9 @@ public class IO
this.read=read;
this.write=write;
}
/* ------------------------------------------------------------ */
/*
/*
* @see java.lang.Runnable#run()
*/
public void run()
@ -109,19 +109,19 @@ public class IO
}
}
}
/* ------------------------------------------------------------------- */
/** Copy Stream in to Stream out until EOF or exception.
* @param in the input stream to read from (until EOF)
* @param out the output stream to write to
* @throws IOException if unable to copy streams
* @throws IOException if unable to copy streams
*/
public static void copy(InputStream in, OutputStream out)
throws IOException
{
copy(in,out,-1);
}
/* ------------------------------------------------------------------- */
/** Copy Reader to Writer out until EOF or exception.
* @param in the read to read from (until EOF)
@ -133,7 +133,7 @@ public class IO
{
copy(in,out,-1);
}
/* ------------------------------------------------------------------- */
/** Copy Stream in to Stream for byteCount bytes or until EOF or exception.
* @param in the stream to read from
@ -145,20 +145,20 @@ public class IO
OutputStream out,
long byteCount)
throws IOException
{
{
byte buffer[] = new byte[bufferSize];
int len=bufferSize;
if (byteCount>=0)
{
while (byteCount>0)
{
int max = byteCount<bufferSize?(int)byteCount:bufferSize;
len=in.read(buffer,0,max);
if (len==-1)
break;
byteCount -= len;
out.write(buffer,0,len);
}
@ -173,8 +173,8 @@ public class IO
out.write(buffer,0,len);
}
}
}
}
/* ------------------------------------------------------------------- */
/** Copy Reader to Writer for byteCount bytes or until EOF or exception.
* @param in the Reader to read from
@ -186,10 +186,10 @@ public class IO
Writer out,
long byteCount)
throws IOException
{
{
char buffer[] = new char[bufferSize];
int len=bufferSize;
if (byteCount>=0)
{
while (byteCount>0)
@ -197,11 +197,11 @@ public class IO
if (byteCount<bufferSize)
len=in.read(buffer,0,(int)byteCount);
else
len=in.read(buffer,0,bufferSize);
len=in.read(buffer,0,bufferSize);
if (len==-1)
break;
byteCount -= len;
out.write(buffer,0,len);
}
@ -253,7 +253,7 @@ public class IO
}
else
to.mkdirs();
File[] files = from.listFiles();
if (files!=null)
{
@ -266,7 +266,7 @@ public class IO
}
}
}
/* ------------------------------------------------------------ */
public static void copyFile(File from,File to) throws IOException
{
@ -276,7 +276,7 @@ public class IO
copy(in,out);
}
}
/* ------------------------------------------------------------ */
/** Read input stream to string.
* @param in the stream to read from (until EOF)
@ -288,13 +288,13 @@ public class IO
{
return toString(in,(Charset)null);
}
/* ------------------------------------------------------------ */
/** Read input stream to string.
* @param in the stream to read from (until EOF)
* @param encoding the encoding to use (can be null to use default Charset)
* @return the String parsed from the stream
* @throws IOException if unable to read the stream (or handle the charset)
* @throws IOException if unable to read the stream (or handle the charset)
*/
public static String toString(InputStream in,String encoding)
throws IOException
@ -306,7 +306,7 @@ public class IO
* @param in the stream to read from (until EOF)
* @param encoding the Charset to use (can be null to use default Charset)
* @return the String parsed from the stream
* @throws IOException if unable to read the stream (or handle the charset)
* @throws IOException if unable to read the stream (or handle the charset)
*/
public static String toString(InputStream in, Charset encoding)
throws IOException
@ -322,7 +322,7 @@ public class IO
/** Read input stream to string.
* @param in the reader to read from (until EOF)
* @return the String parsed from the reader
* @throws IOException if unable to read the stream (or handle the charset)
* @throws IOException if unable to read the stream (or handle the charset)
*/
public static String toString(Reader in)
throws IOException
@ -351,7 +351,7 @@ public class IO
}
return file.delete();
}
/**
* Closes an arbitrary closable, and logs exceptions at ignore level
*
@ -379,7 +379,7 @@ public class IO
{
close((Closeable)is);
}
/**
* closes an output stream, and logs exceptions
*
@ -392,7 +392,7 @@ public class IO
/**
* closes a reader, and logs exceptions
*
*
* @param reader the reader to close
*/
public static void close(Reader reader)
@ -402,14 +402,14 @@ public class IO
/**
* closes a writer, and logs exceptions
*
*
* @param writer the writer to close
*/
public static void close(Writer writer)
{
close((Closeable)writer);
}
/* ------------------------------------------------------------ */
public static byte[] readBytes(InputStream in)
throws IOException
@ -426,7 +426,7 @@ public class IO
* This method wraps a gather write with a loop that handles the limitations of some operating systems that have a
* limit on the number of buffers written. The method loops on the write until either all the content is written or
* no progress is made.
*
*
* @param out
* The GatheringByteChannel to write to
* @param buffers
@ -446,14 +446,14 @@ public class IO
{
// Write as much as we can
long wrote=out.write(buffers,offset,length);
// If we can't write any more, give up
if (wrote==0)
break;
// count the total
total+=wrote;
// Look for unwritten content
for (int i=offset;i<buffers.length;i++)
{
@ -467,25 +467,10 @@ public class IO
}
length=0;
}
return total;
}
/* ------------------------------------------------------------ */
/**
* @param name A host name like www.foo.com
* @param domain A domain name like foo.com
* @return True if the host name is in the domain name
*/
public static boolean isInDomain(String name, String domain)
{
if (!name.endsWith(domain))
return false;
if (name.length()==domain.length())
return true;
return name.charAt(name.length()-domain.length()-1)=='.';
}
/* ------------------------------------------------------------ */
/**
* @return An outputstream to nowhere
@ -496,17 +481,17 @@ public class IO
}
/* ------------------------------------------------------------ */
/**
/**
* @return An outputstream to nowhere
*/
public static InputStream getClosedStream()
{
return __closedStream;
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
private static class NullOS extends OutputStream
private static class NullOS extends OutputStream
{
@Override
public void close(){}
@ -521,10 +506,10 @@ public class IO
}
private static NullOS __nullStream = new NullOS();
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
private static class ClosedIS extends InputStream
private static class ClosedIS extends InputStream
{
@Override
public int read() throws IOException
@ -533,28 +518,28 @@ public class IO
}
}
private static ClosedIS __closedStream = new ClosedIS();
/* ------------------------------------------------------------ */
/**
/**
* @return An writer to nowhere
*/
public static Writer getNullWriter()
{
return __nullWriter;
}
/* ------------------------------------------------------------ */
/**
/**
* @return An writer to nowhere
*/
public static PrintWriter getNullPrintWriter()
{
return __nullPrintWriter;
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
private static class NullWrite extends Writer
private static class NullWrite extends Writer
{
@Override
public void close(){}

View File

@ -24,6 +24,7 @@ import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.net.ssl.SNIMatcher;
import javax.net.ssl.SSLEngine;
@ -41,9 +42,9 @@ import org.eclipse.jetty.util.log.Logger;
*/
public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
{
public static final String SNI_NAME = "org.eclipse.jetty.util.ssl.sniname";
public static final String SNI_WILD = "org.eclipse.jetty.util.ssl.sniwild";
public static final String NO_MATCHERS="No Matchers";
public static final String SNI_HOSTS = "org.eclipse.jetty.util.ssl.snihosts";
public static final String SNI_WILDS = "org.eclipse.jetty.util.ssl.sniwilds";
private static final String NO_MATCHERS = "no_matchers";
private static final Logger LOG = Log.getLogger(SniX509ExtendedKeyManager.class);
private final X509ExtendedKeyManager _delegate;
@ -72,10 +73,11 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
if (aliases==null || aliases.length==0)
return null;
// Look for an SNI alias
String alias=null;
// Look for the SNI information.
String host=null;
String wild=null;
String alias=null;
List<String> hosts=null;
List<String> wilds=null;
if (matchers!=null)
{
for (SNIMatcher m : matchers)
@ -83,16 +85,17 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
if (m instanceof SslContextFactory.AliasSNIMatcher)
{
SslContextFactory.AliasSNIMatcher matcher = (SslContextFactory.AliasSNIMatcher)m;
host=matcher.getHost();
alias=matcher.getAlias();
host=matcher.getServerName();
wild=matcher.getWildDomain();
hosts=matcher.getHosts();
wilds=matcher.getWilds();
break;
}
}
}
if (LOG.isDebugEnabled())
LOG.debug("matched {}/{} from {}",alias,host,Arrays.asList(aliases));
LOG.debug("Matched {} with alias {}/{}/{} from {}",host,alias,hosts,wilds,Arrays.asList(aliases));
// Check if the SNI selected alias is allowable
if (alias!=null)
@ -101,9 +104,8 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
{
if (a.equals(alias))
{
session.putValue(SNI_NAME,host);
if (wild!=null)
session.putValue(SNI_WILD,wild);
session.putValue(SNI_HOSTS,hosts);
session.putValue(SNI_WILDS,wilds);
return alias;
}
}
@ -121,7 +123,7 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
if (alias==NO_MATCHERS)
alias=_delegate.chooseServerAlias(keyType,issuers,socket);
if (LOG.isDebugEnabled())
LOG.debug("chose {}/{} on {}",alias,keyType,socket);
LOG.debug("Chose alias {}/{} on {}",alias,keyType,socket);
return alias;
}
@ -132,7 +134,7 @@ public class SniX509ExtendedKeyManager extends X509ExtendedKeyManager
if (alias==NO_MATCHERS)
alias=_delegate.chooseEngineServerAlias(keyType,issuers,engine);
if (LOG.isDebugEnabled())
LOG.debug("chose {}/{} on {}",alias,keyType,engine);
LOG.debug("Chose alias {}/{} on {}",alias,keyType,engine);
return alias;
}

View File

@ -47,6 +47,7 @@ import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
@ -149,7 +150,7 @@ public class SslContextFactory extends AbstractLifeCycle
private final Set<String> _excludeCipherSuites = new LinkedHashSet<>();
/** Included cipher suites. */
private final List<String> _includeCipherSuites = new ArrayList<String>();
private final List<String> _includeCipherSuites = new ArrayList<>();
private boolean _useCipherSuitesOrder=true;
/** Cipher comparator for ordering ciphers */
@ -167,7 +168,7 @@ public class SslContextFactory extends AbstractLifeCycle
/** SSL certificate alias */
private String _certAlias;
private final Map<String,String> _certAliases = new HashMap<>();
private final Map<String,String> _certHosts = new HashMap<>();
private final Map<String,String> _certWilds = new HashMap<>();
/** Truststore path */
@ -371,7 +372,7 @@ public class SslContextFactory extends AbstractLifeCycle
}
// Look for X.509 certificates to create alias map
_certAliases.clear();
_certHosts.clear();
if (keyStore!=null)
{
for (String alias : Collections.list(keyStore.aliases()))
@ -404,7 +405,7 @@ public class SslContextFactory extends AbstractLifeCycle
if (cn!=null)
{
named=true;
_certAliases.put(cn,alias);
_certHosts.put(cn,alias);
}
}
}
@ -422,7 +423,7 @@ public class SslContextFactory extends AbstractLifeCycle
if (LOG.isDebugEnabled())
LOG.debug("Certificate cn alias={} cn={} in {}",alias,cn,this);
if (cn!=null && cn.contains(".") && !cn.contains(" "))
_certAliases.put(cn,alias);
_certHosts.put(cn,alias);
}
}
}
@ -432,11 +433,18 @@ public class SslContextFactory extends AbstractLifeCycle
// find wild aliases
_certWilds.clear();
for (String name : _certAliases.keySet())
if (name.startsWith("*."))
_certWilds.put(name.substring(2),_certAliases.get(name));
for (Iterator<Map.Entry<String, String>> iterator = _certHosts.entrySet().iterator(); iterator.hasNext();)
{
Map.Entry<String, String> entry = iterator.next();
String host = entry.getKey();
if (host.startsWith("*."))
{
_certWilds.put(host.substring(2), entry.getValue());
iterator.remove();
}
}
LOG.info("x509={} wild={} alias={} for {}",_certAliases,_certWilds,_certAlias,this);
LOG.info("x509={} wild={} alias={} for {}",_certHosts,_certWilds,_certAlias,this);
// Instantiate key and trust managers
KeyManager[] keyManagers = getKeyManagers(keyStore);
@ -470,7 +478,7 @@ public class SslContextFactory extends AbstractLifeCycle
{
_factory = null;
super.doStop();
_certAliases.clear();
_certHosts.clear();
_certWilds.clear();
}
@ -1140,7 +1148,7 @@ public class SslContextFactory extends AbstractLifeCycle
}
}
if (!_certAliases.isEmpty() || !_certWilds.isEmpty())
if (!_certHosts.isEmpty() || !_certWilds.isEmpty())
{
for (int idx = 0; idx < managers.length; idx++)
{
@ -1615,7 +1623,7 @@ public class SslContextFactory extends AbstractLifeCycle
SSLParameters sslParams = sslEngine.getSSLParameters();
sslParams.setEndpointIdentificationAlgorithm(_endpointIdentificationAlgorithm);
sslParams.setUseCipherSuitesOrder(_useCipherSuitesOrder);
if (!_certAliases.isEmpty() || !_certWilds.isEmpty())
if (!_certHosts.isEmpty() || !_certWilds.isEmpty())
{
if (LOG.isDebugEnabled())
LOG.debug("Enable SNI matching {}",sslEngine);
@ -1752,78 +1760,96 @@ public class SslContextFactory extends AbstractLifeCycle
class AliasSNIMatcher extends SNIMatcher
{
private String _host;
private String _alias;
private String _wild;
private SNIHostName _name;
private List<String> _hosts;
private List<String> _wilds;
protected AliasSNIMatcher()
AliasSNIMatcher()
{
super(StandardConstants.SNI_HOST_NAME);
_hosts = Collections.emptyList();
_wilds = Collections.emptyList();
}
@Override
public boolean matches(SNIServerName serverName)
{
LOG.debug("matches={} for {}",serverName,this);
if (LOG.isDebugEnabled())
LOG.debug("SNI matching for {}",serverName);
if (serverName instanceof SNIHostName)
{
_name=(SNIHostName)serverName;
String host = _host = ((SNIHostName)serverName).getAsciiName();
// If we don't have a SNI name, or didn't see any certificate aliases,
// just say true as it will either somehow work or fail elsewhere
if (_certAliases.size()==0)
// If we don't have a SNI host, or didn't see any certificate aliases,
// just say true as it will either somehow work or fail elsewhere.
if (_certHosts.isEmpty())
return true;
// Try an exact match
_alias = _certAliases.get(_name.getAsciiName());
_alias = _certHosts.get(host);
if (_alias!=null)
{
_hosts = _certHosts.entrySet().stream()
.filter(entry -> _alias.equals(entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (LOG.isDebugEnabled())
LOG.debug("matched {}->{}",_name.getAsciiName(),_alias);
return true;
LOG.debug("SNI matched {}->{}->{}",host,_alias,_hosts);
}
// Try wild card matches
String domain = _name.getAsciiName();
_alias = _certWilds.get(domain);
// Try wildcard matches.
if (_alias==null)
{
int dot=domain.indexOf('.');
if (dot>=0)
_alias = _certWilds.get(host);
if (_alias==null)
{
domain=domain.substring(dot+1);
_alias = _certWilds.get(domain);
int dot=host.indexOf('.');
if (dot>=0)
{
host=host.substring(dot+1);
_alias = _certWilds.get(host);
}
}
}
if (_alias!=null)
{
_wild=domain;
_wilds = _certWilds.entrySet().stream()
.filter(entry -> _alias.equals(entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (LOG.isDebugEnabled())
LOG.debug("wild match {}->{}",_name.getAsciiName(),_alias);
LOG.debug("SNI wild match {}->{}->{}",host,_alias,_wilds);
return true;
}
}
if (LOG.isDebugEnabled())
LOG.debug("No match for {}",_name.getAsciiName());
LOG.debug("SNI no match for {}", serverName);
// Return true and allow the KeyManager to accept or reject when choosing a certificate.
return true;
}
public String getHost()
{
return _host;
}
public String getAlias()
{
return _alias;
}
public String getWildDomain()
public List<String> getHosts()
{
return _wild;
return _hosts;
}
public String getServerName()
public List<String> getWilds()
{
return _name==null?null:_name.getAsciiName();
return _wilds;
}
}
}