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 }