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