View Javadoc
1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.server;
20  
21  import java.security.cert.X509Certificate;
22  import java.util.concurrent.TimeUnit;
23  
24  import javax.net.ssl.SSLContext;
25  import javax.net.ssl.SSLEngine;
26  import javax.net.ssl.SSLSession;
27  import javax.servlet.ServletRequest;
28  
29  import org.eclipse.jetty.http.BadMessageException;
30  import org.eclipse.jetty.http.HttpField;
31  import org.eclipse.jetty.http.HttpHeader;
32  import org.eclipse.jetty.http.HttpScheme;
33  import org.eclipse.jetty.io.EndPoint;
34  import org.eclipse.jetty.http.PreEncodedHttpField;
35  import org.eclipse.jetty.io.ssl.SslConnection;
36  import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint;
37  import org.eclipse.jetty.util.TypeUtil;
38  import org.eclipse.jetty.util.annotation.Name;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.util.log.Logger;
41  import org.eclipse.jetty.util.ssl.SniX509ExtendedKeyManager;
42  import org.eclipse.jetty.util.ssl.SslContextFactory;
43  import org.eclipse.jetty.util.ssl.X509;
44  
45  /**
46   * <p>Customizer that extracts the attribute from an {@link SSLContext}
47   * and sets them on the request with {@link ServletRequest#setAttribute(String, Object)}
48   * according to Servlet Specification Requirements.</p>
49   */
50  public class SecureRequestCustomizer implements HttpConfiguration.Customizer
51  {
52      private static final Logger LOG = Log.getLogger(SecureRequestCustomizer.class);
53  
54      /**
55       * The name of the SSLSession attribute that will contain any cached information.
56       */
57      public static final String CACHED_INFO_ATTR = CachedInfo.class.getName();
58  
59      private String sslSessionAttribute = "org.eclipse.jetty.servlet.request.ssl_session";
60  
61      private boolean _sniHostCheck;
62      private long _stsMaxAge=-1;
63      private boolean _stsIncludeSubDomains;
64      private HttpField _stsField;
65  
66      public SecureRequestCustomizer()
67      {
68          this(true);
69      }
70  
71      public SecureRequestCustomizer(@Name("sniHostCheck")boolean sniHostCheck)
72      {
73          this(sniHostCheck,-1,false);
74      }
75      
76      /**
77       * @param sniHostCheck True if the SNI Host name must match.
78       * @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
79       * @param stsIncludeSubdomains If true, a include subdomain property is sent with any Strict-Transport-Security header
80       */
81      public SecureRequestCustomizer(
82              @Name("sniHostCheck")boolean sniHostCheck,
83              @Name("stsMaxAgeSeconds")long stsMaxAgeSeconds,
84              @Name("stsIncludeSubdomains")boolean stsIncludeSubdomains)
85      {
86          _sniHostCheck=sniHostCheck;
87          _stsMaxAge=stsMaxAgeSeconds;
88          _stsIncludeSubDomains=stsIncludeSubdomains;
89          formatSTS();
90      }
91  
92      /**
93       * @return True if the SNI Host name must match.
94       */
95      public boolean isSniHostCheck()
96      {
97          return _sniHostCheck;
98      }
99  
100     /**
101      * @param sniHostCheck  True if the SNI Host name must match. 
102      */
103     public void setSniHostCheck(boolean sniHostCheck)
104     {
105         _sniHostCheck = sniHostCheck;
106     }
107 
108     /**
109      * @return The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
110      */
111     public long getStsMaxAge()
112     {
113         return _stsMaxAge;
114     }
115 
116     /**
117      * Set the Strict-Transport-Security max age.
118      * @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent.
119      */
120     public void setStsMaxAge(long stsMaxAgeSeconds)
121     {
122         _stsMaxAge = stsMaxAgeSeconds;
123         formatSTS();
124     }
125 
126     /**
127      * Convenience method to call {@link #setStsMaxAge(long)}
128      * @param period The period in units
129      * @param units The {@link TimeUnit} of the period
130      */
131     public void setStsMaxAge(long period,TimeUnit units)
132     {
133         _stsMaxAge = units.toSeconds(period);
134         formatSTS();
135     }
136 
137     /**
138      * @return true if a include subdomain property is sent with any Strict-Transport-Security header
139      */
140     public boolean isStsIncludeSubDomains()
141     {
142         return _stsIncludeSubDomains;
143     }
144 
145     /**
146      * @param stsIncludeSubDomains If true, a include subdomain property is sent with any Strict-Transport-Security header
147      */
148     public void setStsIncludeSubDomains(boolean stsIncludeSubDomains)
149     {
150         _stsIncludeSubDomains = stsIncludeSubDomains;
151         formatSTS();
152     }
153 
154     private void formatSTS()
155     {
156         if (_stsMaxAge<0)
157             _stsField=null;
158         else
159             _stsField=new PreEncodedHttpField(HttpHeader.STRICT_TRANSPORT_SECURITY,String.format("max-age=%d%s",_stsMaxAge,_stsIncludeSubDomains?"; includeSubDomains":""));
160     }
161 
162     @Override
163     public void customize(Connector connector, HttpConfiguration channelConfig, Request request)
164     {
165         EndPoint endp = request.getHttpChannel().getEndPoint();
166         if (endp instanceof DecryptedEndPoint)
167         {
168             SslConnection.DecryptedEndPoint ssl_endp = (DecryptedEndPoint)endp;
169             SslConnection sslConnection = ssl_endp.getSslConnection();
170             SSLEngine sslEngine=sslConnection.getSSLEngine();
171             customize(sslEngine,request);
172 
173             if (request.getHttpURI().getScheme()==null)
174                 request.setScheme(HttpScheme.HTTPS.asString());
175         }
176         else if (endp instanceof ProxyConnectionFactory.ProxyEndPoint)
177         {
178             ProxyConnectionFactory.ProxyEndPoint proxy = (ProxyConnectionFactory.ProxyEndPoint)endp;
179             if (request.getHttpURI().getScheme()==null && proxy.getAttribute(ProxyConnectionFactory.TLS_VERSION)!=null)
180                 request.setScheme(HttpScheme.HTTPS.asString());       
181         }
182 
183         if (HttpScheme.HTTPS.is(request.getScheme()))
184             customizeSecure(request);
185     }
186 
187     /**
188      * Customizes the request attributes for general secure settings.
189      * The default impl calls {@link Request#setSecure(boolean)} with true
190      * and sets a response header if the Strict-Transport-Security options 
191      * are set.
192      * @param request the request being customized
193      */
194     protected void customizeSecure(Request request)
195     {
196         request.setSecure(true);
197         
198         if (_stsField!=null)
199             request.getResponse().getHttpFields().add(_stsField);
200     }
201 
202     /**
203      * <p>
204      * Customizes the request attributes to be set for SSL requests.
205      * </p>
206      * <p>
207      * The requirements of the Servlet specs are:
208      * </p>
209      * <ul>
210      * <li>an attribute named "javax.servlet.request.ssl_session_id" of type String (since Servlet Spec 3.0).</li>
211      * <li>an attribute named "javax.servlet.request.cipher_suite" of type String.</li>
212      * <li>an attribute named "javax.servlet.request.key_size" of type Integer.</li>
213      * <li>an attribute named "javax.servlet.request.X509Certificate" of type java.security.cert.X509Certificate[]. This
214      * is an array of objects of type X509Certificate, the order of this array is defined as being in ascending order of
215      * trust. The first certificate in the chain is the one set by the client, the next is the one used to authenticate
216      * the first, and so on.</li>
217      * </ul>
218      * 
219      * @param sslEngine
220      *            the sslEngine to be customized.
221      * @param request
222      *            HttpRequest to be customized.
223      */
224     protected void customize(SSLEngine sslEngine, Request request)
225     {
226         SSLSession sslSession = sslEngine.getSession();
227 
228         if (_sniHostCheck)
229         {
230             String name = request.getServerName();
231             X509 x509 = (X509)sslSession.getValue(SniX509ExtendedKeyManager.SNI_X509);
232 
233             if (x509!=null && !x509.matches(name))
234             {
235                 LOG.warn("Host {} does not match SNI {}",name,x509);
236                 throw new BadMessageException(400,"Host does not match SNI");
237             }
238 
239             if (LOG.isDebugEnabled())
240                 LOG.debug("Host {} matched SNI {}",name,x509);
241         }
242 
243         try
244         {
245             String cipherSuite=sslSession.getCipherSuite();
246             Integer keySize;
247             X509Certificate[] certs;
248             String idStr;
249 
250             CachedInfo cachedInfo=(CachedInfo)sslSession.getValue(CACHED_INFO_ATTR);
251             if (cachedInfo!=null)
252             {
253                 keySize=cachedInfo.getKeySize();
254                 certs=cachedInfo.getCerts();
255                 idStr=cachedInfo.getIdStr();
256             }
257             else
258             {
259                 keySize=SslContextFactory.deduceKeyLength(cipherSuite);
260                 certs=SslContextFactory.getCertChain(sslSession);
261                 byte[] bytes = sslSession.getId();
262                 idStr = TypeUtil.toHexString(bytes);
263                 cachedInfo=new CachedInfo(keySize,certs,idStr);
264                 sslSession.putValue(CACHED_INFO_ATTR,cachedInfo);
265             }
266 
267             if (certs!=null)
268                 request.setAttribute("javax.servlet.request.X509Certificate",certs);
269 
270             request.setAttribute("javax.servlet.request.cipher_suite",cipherSuite);
271             request.setAttribute("javax.servlet.request.key_size",keySize);
272             request.setAttribute("javax.servlet.request.ssl_session_id", idStr);
273             String sessionAttribute = getSslSessionAttribute();
274             if (sessionAttribute != null && !sessionAttribute.isEmpty())
275                 request.setAttribute(sessionAttribute, sslSession);
276         }
277         catch (Exception e)
278         {
279             LOG.warn(Log.EXCEPTION,e);
280         }
281     }
282     
283     public void setSslSessionAttribute(String attribute)
284     {
285         this.sslSessionAttribute = attribute;
286     }
287 
288     public String getSslSessionAttribute()
289     {
290         return sslSessionAttribute;
291     }
292 
293     @Override
294     public String toString()
295     {
296         return String.format("%s@%x",this.getClass().getSimpleName(),hashCode());
297     }
298 
299     /**
300      * Simple bundle of information that is cached in the SSLSession. Stores the
301      * effective keySize and the client certificate chain.
302      */
303     private static class CachedInfo
304     {
305         private final X509Certificate[] _certs;
306         private final Integer _keySize;
307         private final String _idStr;
308 
309         CachedInfo(Integer keySize, X509Certificate[] certs,String idStr)
310         {
311             this._keySize=keySize;
312             this._certs=certs;
313             this._idStr=idStr;
314         }
315 
316         X509Certificate[] getCerts()
317         {
318             return _certs;
319         }
320 
321         Integer getKeySize()
322         {
323             return _keySize;
324         }
325 
326         String getIdStr()
327         {
328             return _idStr;
329         }
330     }
331 }