blob: c683ab1232413b27e383f79a113edcffacf0613a [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 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 java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
import javax.servlet.ServletRequest;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.annotation.Name;
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;
import org.eclipse.jetty.util.ssl.X509;
/**
* <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.</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 String sslSessionAttribute = "org.eclipse.jetty.servlet.request.ssl_session";
private boolean _sniHostCheck;
private long _stsMaxAge=-1;
private boolean _stsIncludeSubDomains;
private HttpField _stsField;
public SecureRequestCustomizer()
{
this(true);
}
public SecureRequestCustomizer(@Name("sniHostCheck")boolean sniHostCheck)
{
this(sniHostCheck,-1,false);
}
/**
* @param sniHostCheck True if the SNI Host name must match.
* @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
* @param stsIncludeSubdomains If true, a include subdomain property is sent with any Strict-Transport-Security header
*/
public SecureRequestCustomizer(
@Name("sniHostCheck")boolean sniHostCheck,
@Name("stsMaxAgeSeconds")long stsMaxAgeSeconds,
@Name("stsIncludeSubdomains")boolean stsIncludeSubdomains)
{
_sniHostCheck=sniHostCheck;
_stsMaxAge=stsMaxAgeSeconds;
_stsIncludeSubDomains=stsIncludeSubdomains;
formatSTS();
}
/**
* @return True if the SNI Host name must match.
*/
public boolean isSniHostCheck()
{
return _sniHostCheck;
}
/**
* @param sniHostCheck True if the SNI Host name must match.
*/
public void setSniHostCheck(boolean sniHostCheck)
{
_sniHostCheck = sniHostCheck;
}
/**
* @return The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
*/
public long getStsMaxAge()
{
return _stsMaxAge;
}
/**
* Set the Strict-Transport-Security max age.
* @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
*/
public void setStsMaxAge(long stsMaxAgeSeconds)
{
_stsMaxAge = stsMaxAgeSeconds;
formatSTS();
}
/**
* Convenience method to call {@link #setStsMaxAge(long)}
* @param period The period in units
* @param units The {@link TimeUnit} of the period
*/
public void setStsMaxAge(long period,TimeUnit units)
{
_stsMaxAge = units.toSeconds(period);
formatSTS();
}
/**
* @return true if a include subdomain property is sent with any Strict-Transport-Security header
*/
public boolean isStsIncludeSubDomains()
{
return _stsIncludeSubDomains;
}
/**
* @param stsIncludeSubDomains If true, a include subdomain property is sent with any Strict-Transport-Security header
*/
public void setStsIncludeSubDomains(boolean stsIncludeSubDomains)
{
_stsIncludeSubDomains = stsIncludeSubDomains;
formatSTS();
}
private void formatSTS()
{
if (_stsMaxAge<0)
_stsField=null;
else
_stsField=new PreEncodedHttpField(HttpHeader.STRICT_TRANSPORT_SECURITY,String.format("max-age=%d%s",_stsMaxAge,_stsIncludeSubDomains?"; includeSubDomains":""));
}
@Override
public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
{
if (request.getHttpChannel().getEndPoint() instanceof DecryptedEndPoint)
{
if (request.getHttpURI().getScheme()==null)
request.setScheme(HttpScheme.HTTPS.asString());
SslConnection.DecryptedEndPoint ssl_endp = (DecryptedEndPoint)request.getHttpChannel().getEndPoint();
SslConnection sslConnection = ssl_endp.getSslConnection();
SSLEngine sslEngine=sslConnection.getSSLEngine();
customize(sslEngine,request);
}
if (HttpScheme.HTTPS.is(request.getScheme()))
customizeSecure(request);
}
/**
* Customizes the request attributes for general secure settings.
* The default impl calls {@link Request#setSecure(boolean)} with true
* and sets a response header if the Strict-Transport-Security options
* are set.
* @param request the request being customized
*/
protected void customizeSecure(Request request)
{
request.setSecure(true);
if (_stsField!=null)
request.getResponse().getHttpFields().add(_stsField);
}
/**
* <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>
* <li>an attribute named "javax.servlet.request.cipher_suite" of type String.</li>
* <li>an attribute named "javax.servlet.request.key_size" of type Integer.</li>
* <li>an attribute named "javax.servlet.request.X509Certificate" of type java.security.cert.X509Certificate[]. This
* is an array of objects of type X509Certificate, the order of this array is defined as being in ascending order of
* trust. The first certificate in the chain is the one set by the client, the next is the one used to authenticate
* the first, and so on.</li>
* </ul>
*
* @param sslEngine
* the sslEngine to be customized.
* @param request
* HttpRequest to be customized.
*/
protected void customize(SSLEngine sslEngine, Request request)
{
request.setScheme(HttpScheme.HTTPS.asString());
SSLSession sslSession = sslEngine.getSession();
if (_sniHostCheck)
{
String name = request.getServerName();
X509 x509 = (X509)sslSession.getValue(SniX509ExtendedKeyManager.SNI_X509);
if (x509!=null && !x509.matches(name))
{
LOG.warn("Host {} does not match SNI {}",name,x509);
throw new BadMessageException(400,"Host does not match SNI");
}
if (LOG.isDebugEnabled())
LOG.debug("Host {} matched SNI {}",name,x509);
}
try
{
String cipherSuite=sslSession.getCipherSuite();
Integer keySize;
X509Certificate[] certs;
String idStr;
CachedInfo cachedInfo=(CachedInfo)sslSession.getValue(CACHED_INFO_ATTR);
if (cachedInfo!=null)
{
keySize=cachedInfo.getKeySize();
certs=cachedInfo.getCerts();
idStr=cachedInfo.getIdStr();
}
else
{
keySize=SslContextFactory.deduceKeyLength(cipherSuite);
certs=SslContextFactory.getCertChain(sslSession);
byte[] bytes = sslSession.getId();
idStr = TypeUtil.toHexString(bytes);
cachedInfo=new CachedInfo(keySize,certs,idStr);
sslSession.putValue(CACHED_INFO_ATTR,cachedInfo);
}
if (certs!=null)
request.setAttribute("javax.servlet.request.X509Certificate",certs);
request.setAttribute("javax.servlet.request.cipher_suite",cipherSuite);
request.setAttribute("javax.servlet.request.key_size",keySize);
request.setAttribute("javax.servlet.request.ssl_session_id", idStr);
String sessionAttribute = getSslSessionAttribute();
if (sessionAttribute != null && !sessionAttribute.isEmpty())
request.setAttribute(sessionAttribute, sslSession);
}
catch (Exception e)
{
LOG.warn(Log.EXCEPTION,e);
}
}
public void setSslSessionAttribute(String attribute)
{
this.sslSessionAttribute = attribute;
}
public String getSslSessionAttribute()
{
return sslSessionAttribute;
}
@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.
*/
private static class CachedInfo
{
private final X509Certificate[] _certs;
private final Integer _keySize;
private final String _idStr;
CachedInfo(Integer keySize, X509Certificate[] certs,String idStr)
{
this._keySize=keySize;
this._certs=certs;
this._idStr=idStr;
}
X509Certificate[] getCerts()
{
return _certs;
}
Integer getKeySize()
{
return _keySize;
}
String getIdStr()
{
return _idStr;
}
}
}