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 */
017package org.apache.hadoop.security;
018
019import java.io.IOException;
020import java.net.InetAddress;
021import java.net.ServerSocket;
022import java.security.Principal;
023import java.util.Collections;
024import java.util.List;
025import java.util.Random;
026
027import javax.net.ssl.SSLContext;
028import javax.net.ssl.SSLServerSocket;
029import javax.net.ssl.SSLServerSocketFactory;
030import javax.net.ssl.SSLSocket;
031import javax.security.auth.kerberos.KerberosPrincipal;
032import javax.servlet.Filter;
033import javax.servlet.FilterChain;
034import javax.servlet.FilterConfig;
035import javax.servlet.ServletException;
036import javax.servlet.ServletRequest;
037import javax.servlet.ServletResponse;
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletRequestWrapper;
040import javax.servlet.http.HttpServletResponse;
041
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.mortbay.io.EndPoint;
045import org.mortbay.jetty.HttpSchemes;
046import org.mortbay.jetty.Request;
047import org.mortbay.jetty.security.ServletSSL;
048import 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 */
056public 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}