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