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 }