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     /**
198      * <p>
199      * Customizes the request attributes to be set for SSL requests.
200      * </p>
201      * <p>
202      * The requirements of the Servlet specs are:
203      * </p>
204      * <ul>
205      * <li>an attribute named "javax.servlet.request.ssl_session_id" of type String (since Servlet Spec 3.0).</li>
206      * <li>an attribute named "javax.servlet.request.cipher_suite" of type String.</li>
207      * <li>an attribute named "javax.servlet.request.key_size" of type Integer.</li>
208      * <li>an attribute named "javax.servlet.request.X509Certificate" of type java.security.cert.X509Certificate[]. This
209      * is an array of objects of type X509Certificate, the order of this array is defined as being in ascending order of
210      * trust. The first certificate in the chain is the one set by the client, the next is the one used to authenticate
211      * the first, and so on.</li>
212      * </ul>
213      * 
214      * @param sslEngine
215      *            the sslEngine to be customized.
216      * @param request
217      *            HttpRequest to be customized.
218      */
219     protected void customize(SSLEngine sslEngine, Request request)
220     {
221         request.setScheme(HttpScheme.HTTPS.asString());
222         SSLSession sslSession = sslEngine.getSession();
223 
224         if (_sniHostCheck)
225         {
226             String name = request.getServerName();
227             X509 x509 = (X509)sslSession.getValue(SniX509ExtendedKeyManager.SNI_X509);
228 
229             if (x509!=null && !x509.matches(name))
230             {
231                 LOG.warn("Host {} does not match SNI {}",name,x509);
232                 throw new BadMessageException(400,"Host does not match SNI");
233             }
234 
235             if (LOG.isDebugEnabled())
236                 LOG.debug("Host {} matched SNI {}",name,x509);
237         }
238 
239         try
240         {
241             String cipherSuite=sslSession.getCipherSuite();
242             Integer keySize;
243             X509Certificate[] certs;
244             String idStr;
245 
246             CachedInfo cachedInfo=(CachedInfo)sslSession.getValue(CACHED_INFO_ATTR);
247             if (cachedInfo!=null)
248             {
249                 keySize=cachedInfo.getKeySize();
250                 certs=cachedInfo.getCerts();
251                 idStr=cachedInfo.getIdStr();
252             }
253             else
254             {
255                 keySize=SslContextFactory.deduceKeyLength(cipherSuite);
256                 certs=SslContextFactory.getCertChain(sslSession);
257                 byte[] bytes = sslSession.getId();
258                 idStr = TypeUtil.toHexString(bytes);
259                 cachedInfo=new CachedInfo(keySize,certs,idStr);
260                 sslSession.putValue(CACHED_INFO_ATTR,cachedInfo);
261             }
262 
263             if (certs!=null)
264                 request.setAttribute("javax.servlet.request.X509Certificate",certs);
265 
266             request.setAttribute("javax.servlet.request.cipher_suite",cipherSuite);
267             request.setAttribute("javax.servlet.request.key_size",keySize);
268             request.setAttribute("javax.servlet.request.ssl_session_id", idStr);
269             request.setAttribute(getSslSessionAttribute(), sslSession);
270         }
271         catch (Exception e)
272         {
273             LOG.warn(Log.EXCEPTION,e);
274         }
275     }
276     
277     public void setSslSessionAttribute(String attribute)
278     {
279         this.sslSessionAttribute = attribute;
280     }
281 
282     public String getSslSessionAttribute()
283     {
284         return sslSessionAttribute;
285     }
286 
287     @Override
288     public String toString()
289     {
290         return String.format("%s@%x",this.getClass().getSimpleName(),hashCode());
291     }
292 
293     /**
294      * Simple bundle of information that is cached in the SSLSession. Stores the
295      * effective keySize and the client certificate chain.
296      */
297     private static class CachedInfo
298     {
299         private final X509Certificate[] _certs;
300         private final Integer _keySize;
301         private final String _idStr;
302 
303         CachedInfo(Integer keySize, X509Certificate[] certs,String idStr)
304         {
305             this._keySize=keySize;
306             this._certs=certs;
307             this._idStr=idStr;
308         }
309 
310         X509Certificate[] getCerts()
311         {
312             return _certs;
313         }
314 
315         Integer getKeySize()
316         {
317             return _keySize;
318         }
319 
320         String getIdStr()
321         {
322             return _idStr;
323         }
324     }
325 }