001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements. See the NOTICE file distributed with this
004     * work for additional information regarding copyright ownership. The ASF
005     * licenses this file to you under the Apache License, Version 2.0 (the
006     * "License"); you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     * 
009     * http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014     * License for the specific language governing permissions and limitations under
015     * the License.
016     */
017    package org.apache.hadoop.security;
018    
019    import java.io.IOException;
020    import java.net.InetAddress;
021    import java.net.ServerSocket;
022    import java.security.Principal;
023    import java.util.Collections;
024    import java.util.List;
025    import java.util.Random;
026    
027    import javax.net.ssl.SSLContext;
028    import javax.net.ssl.SSLServerSocket;
029    import javax.net.ssl.SSLServerSocketFactory;
030    import javax.net.ssl.SSLSocket;
031    import javax.security.auth.kerberos.KerberosPrincipal;
032    import javax.servlet.Filter;
033    import javax.servlet.FilterChain;
034    import javax.servlet.FilterConfig;
035    import javax.servlet.ServletException;
036    import javax.servlet.ServletRequest;
037    import javax.servlet.ServletResponse;
038    import javax.servlet.http.HttpServletRequest;
039    import javax.servlet.http.HttpServletRequestWrapper;
040    import javax.servlet.http.HttpServletResponse;
041    
042    import org.apache.commons.logging.Log;
043    import org.apache.commons.logging.LogFactory;
044    import org.mortbay.io.EndPoint;
045    import org.mortbay.jetty.HttpSchemes;
046    import org.mortbay.jetty.Request;
047    import org.mortbay.jetty.security.ServletSSL;
048    import org.mortbay.jetty.security.SslSocketConnector;
049    
050    /**
051     * Extend Jetty's {@link SslSocketConnector} to optionally also provide 
052     * Kerberos5ized SSL sockets.  The only change in behavior from superclass
053     * is that we no longer honor requests to turn off NeedAuthentication when
054     * running with Kerberos support.
055     */
056    public class Krb5AndCertsSslSocketConnector extends SslSocketConnector {
057      public static final List<String> KRB5_CIPHER_SUITES = 
058        Collections.unmodifiableList(Collections.singletonList(
059              "TLS_KRB5_WITH_3DES_EDE_CBC_SHA"));
060      static {
061        System.setProperty("https.cipherSuites", KRB5_CIPHER_SUITES.get(0));
062      }
063      
064      private static final Log LOG = LogFactory
065          .getLog(Krb5AndCertsSslSocketConnector.class);
066    
067      private static final String REMOTE_PRINCIPAL = "remote_principal";
068    
069      public enum MODE {KRB, CERTS, BOTH} // Support Kerberos, certificates or both?
070    
071      private final boolean useKrb;
072      private final boolean useCerts;
073    
074      public Krb5AndCertsSslSocketConnector() {
075        super();
076        useKrb = true;
077        useCerts = false;
078        
079        setPasswords();
080      }
081      
082      public Krb5AndCertsSslSocketConnector(MODE mode) {
083        super();
084        useKrb = mode == MODE.KRB || mode == MODE.BOTH;
085        useCerts = mode == MODE.CERTS || mode == MODE.BOTH;
086        setPasswords();
087        logIfDebug("useKerb = " + useKrb + ", useCerts = " + useCerts);
088      }
089    
090      // If not using Certs, set passwords to random gibberish or else
091      // Jetty will actually prompt the user for some.
092      private void setPasswords() {
093       if(!useCerts) {
094         Random r = new Random();
095         System.setProperty("jetty.ssl.password", String.valueOf(r.nextLong()));
096         System.setProperty("jetty.ssl.keypassword", String.valueOf(r.nextLong()));
097       }
098      }
099      
100      @Override
101      protected SSLServerSocketFactory createFactory() throws Exception {
102        if(useCerts)
103          return super.createFactory();
104        
105        SSLContext context = super.getProvider()==null
106           ? SSLContext.getInstance(super.getProtocol())
107            :SSLContext.getInstance(super.getProtocol(), super.getProvider());
108        context.init(null, null, null);
109        
110        return context.getServerSocketFactory();
111      }
112      
113      /* (non-Javadoc)
114       * @see org.mortbay.jetty.security.SslSocketConnector#newServerSocket(java.lang.String, int, int)
115       */
116      @Override
117      protected ServerSocket newServerSocket(String host, int port, int backlog)
118          throws IOException {
119        logIfDebug("Creating new KrbServerSocket for: " + host);
120        SSLServerSocket ss = null;
121        
122        if(useCerts) // Get the server socket from the SSL super impl
123          ss = (SSLServerSocket)super.newServerSocket(host, port, backlog);
124        else { // Create a default server socket
125          try {
126            ss = (SSLServerSocket)(host == null 
127             ? createFactory().createServerSocket(port, backlog) :
128               createFactory().createServerSocket(port, backlog, InetAddress.getByName(host)));
129          } catch (Exception e)
130          {
131            LOG.warn("Could not create KRB5 Listener", e);
132            throw new IOException("Could not create KRB5 Listener: " + e.toString());
133          }
134        }
135        
136        // Add Kerberos ciphers to this socket server if needed.
137        if(useKrb) {
138          ss.setNeedClientAuth(true);
139          String [] combined;
140          if(useCerts) { // combine the cipher suites
141            String[] certs = ss.getEnabledCipherSuites();
142            combined = new String[certs.length + KRB5_CIPHER_SUITES.size()];
143            System.arraycopy(certs, 0, combined, 0, certs.length);
144            System.arraycopy(KRB5_CIPHER_SUITES.toArray(new String[0]), 0, combined,
145                  certs.length, KRB5_CIPHER_SUITES.size());
146          } else { // Just enable Kerberos auth
147            combined = KRB5_CIPHER_SUITES.toArray(new String[0]);
148          }
149          
150          ss.setEnabledCipherSuites(combined);
151        }
152        
153        return ss;
154      };
155    
156      @Override
157      public void customize(EndPoint endpoint, Request request) throws IOException {
158        if(useKrb) { // Add Kerberos-specific info
159          SSLSocket sslSocket = (SSLSocket)endpoint.getTransport();
160          Principal remotePrincipal = sslSocket.getSession().getPeerPrincipal();
161          logIfDebug("Remote principal = " + remotePrincipal);
162          request.setScheme(HttpSchemes.HTTPS);
163          request.setAttribute(REMOTE_PRINCIPAL, remotePrincipal);
164          
165          if(!useCerts) { // Add extra info that would have been added by super
166            String cipherSuite = sslSocket.getSession().getCipherSuite();
167            Integer keySize = Integer.valueOf(ServletSSL.deduceKeyLength(cipherSuite));;
168            
169            request.setAttribute("javax.servlet.request.cipher_suite", cipherSuite);
170            request.setAttribute("javax.servlet.request.key_size", keySize);
171          } 
172        }
173        
174        if(useCerts) super.customize(endpoint, request);
175      }
176      
177      private void logIfDebug(String s) {
178        if(LOG.isDebugEnabled())
179          LOG.debug(s);
180      }
181      
182      /**
183       * Filter that takes the Kerberos principal identified in the 
184       * {@link Krb5AndCertsSslSocketConnector} and provides it the to the servlet
185       * at runtime, setting the principal and short name.
186       */
187      public static class Krb5SslFilter implements Filter {
188        @Override
189        public void doFilter(ServletRequest req, ServletResponse resp,
190            FilterChain chain) throws IOException, ServletException {
191          final Principal princ = 
192            (Principal)req.getAttribute(Krb5AndCertsSslSocketConnector.REMOTE_PRINCIPAL);
193          
194          if(princ == null || !(princ instanceof KerberosPrincipal)) {
195            // Should never actually get here, since should be rejected at socket
196            // level.
197            LOG.warn("User not authenticated via kerberos from " + req.getRemoteAddr());
198            ((HttpServletResponse)resp).sendError(HttpServletResponse.SC_FORBIDDEN, 
199                "User not authenticated via Kerberos");
200            return;
201          }
202          
203          // Provide principal information for servlet at runtime
204          ServletRequest wrapper = 
205                new HttpServletRequestWrapper((HttpServletRequest) req) {
206            @Override
207            public Principal getUserPrincipal() {
208              return princ;
209            }
210            
211            /* 
212             * Return the full name of this remote user.
213             * @see javax.servlet.http.HttpServletRequestWrapper#getRemoteUser()
214             */
215            @Override 
216            public String getRemoteUser() {
217              return princ.getName();
218            }
219          };
220          
221          chain.doFilter(wrapper, resp);
222        }
223    
224        @Override
225        public void init(FilterConfig arg0) throws ServletException {
226          /* Nothing to do here */
227        }
228    
229        @Override
230        public void destroy() { /* Nothing to do here */ }
231      }
232    }