// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.enterprise.connector.servlet; import com.google.common.base.Objects; import com.google.enterprise.connector.manager.Context; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.HashSet; import java.util.Locale; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import javax.security.auth.x500.X500Principal; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; /** * Filter that stops the filter chain if the client is not granted access, based * on its IP or client certificate. If using client certificates, then the * client must provide a client certificate whose Subject has a Common Name that * appears within the allowed hosts list. If not using client certificates, the * client's IP must match the IP of one of the hosts in the allowed hosts list. * * <p>By default, only gsa.feed.host is in the allowed hosts list. If the * allowedHostsConfigName init parameter is provided, then that value is looked * up in the connectorManagerProperties and is treated as a comma-separated list * of allowed hosts to allow in addition to the GSA. * * <p>By default, client certificates are not used for security. If the * {@code useClientCertificateSecurityConfigName} init parameter is provided, * then that value is looked up in the connectorManagerProperties and is parsed * as a boolean. When {@code true}, it will enable client certificate security * instead of IP-based. */ public class HostnameSecurity implements Filter { private static Logger LOGGER = Logger.getLogger(HostnameSecurity.class.getName()); private FilterConfig filterConfig; private Set<String> allowedAccessCommonNames; private Set<InetAddress> allowedAccessAddresses; private boolean useClientCertificateSecurity; private String gsaHostInUse; @Override public void init(FilterConfig config) { LOGGER.fine("init"); this.filterConfig = config; loadConnectorConfig(filterConfig); LOGGER.info("init done."); } private void loadConnectorConfig(FilterConfig config) { allowedAccessCommonNames = new HashSet<String>(); allowedAccessAddresses = new HashSet<InetAddress>(); Properties props = Context.getInstance().getConnectorManagerProperties(); String useClientCertificateSecurityConfigName = config.getInitParameter("useClientCertificateSecurityConfigName"); this.useClientCertificateSecurity = false; if (useClientCertificateSecurityConfigName != null) { this.useClientCertificateSecurity = Boolean.valueOf(props.getProperty( useClientCertificateSecurityConfigName)); } if (this.useClientCertificateSecurity) { LOGGER.info("Using client certificate-based security"); } else { LOGGER.info("Using IP-based security"); } String gsaFeedHost = props.getProperty(Context.GSA_FEED_HOST_PROPERTY_KEY); gsaHostInUse = gsaFeedHost; if (gsaFeedHost != null) { allowedAccessCommonNames.add(gsaFeedHost.toLowerCase(Locale.ENGLISH)); } String allowedHostsConfigName = config.getInitParameter("allowedHostsConfigName"); String allowedHosts = ""; if (allowedHostsConfigName != null) { allowedHosts = props.getProperty(allowedHostsConfigName, ""); } for (String hostname : allowedHosts.split(",")) { hostname = hostname.trim(); if ("".equals(hostname)) { continue; } allowedAccessCommonNames.add(hostname.toLowerCase(Locale.ENGLISH)); } String filterName = config.getFilterName(); LOGGER.log(Level.CONFIG, "When using client certificates, common names that" + " are permitted in {0}: {1}", new Object[] {filterName, allowedAccessCommonNames}); for (String hostname : allowedAccessCommonNames) { try { InetAddress[] ips = InetAddress.getAllByName(hostname); allowedAccessAddresses.addAll(Arrays.asList(ips)); } catch (UnknownHostException ex) { LOGGER.log(Level.WARNING, "Could not resolve hostname. Not adding it to" + " full access list of IPs: " + hostname, ex); } } LOGGER.log(Level.CONFIG, "When not using client certificates, IPs that are permitted in " + "{0}: {1}", new Object[] {filterName, allowedAccessAddresses}); } @Override public void destroy() {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (isAllowed(request)) { chain.doFilter(request, response); } else { ((HttpServletResponse) response) .sendError(HttpServletResponse.SC_FORBIDDEN); } } protected boolean isAllowed(ServletRequest request) { String currentGsaHost = Context.getInstance().getGsaFeedHost(); if (!Objects.equal(gsaHostInUse, currentGsaHost)) { // The GSA hostname has changed. Update the allowedAccess sets. LOGGER.info("GSA hostname changed; reloading config."); loadConnectorConfig(filterConfig); } if (!useClientCertificateSecurity) { InetAddress addr; try { addr = InetAddress.getByName(request.getRemoteAddr()); } catch (UnknownHostException ex) { throw new AssertionError(ex); } boolean allowed = allowedAccessAddresses.contains(addr); if (!allowed) { LOGGER.log(Level.WARNING, "Denying caller: {0}", addr); } return allowed; } else { if (!request.isSecure()) { LOGGER.log(Level.WARNING, "Denying caller: unencrypted channel"); return false; } Object o = request.getAttribute("javax.servlet.request.X509Certificate"); if (o == null) { LOGGER.log(Level.WARNING, "Denying caller: no client certificate"); return false; } if (!(o instanceof X509Certificate[])) { LOGGER.log(Level.WARNING, "Denying caller: unexpected certificate " + "class: " + o.getClass().getName()); return false; } X509Certificate[] certificate = (X509Certificate[]) o; if (certificate == null || certificate.length < 1) { LOGGER.log(Level.WARNING, "Denying caller: no client certificate"); return false; } X500Principal principal = certificate[0].getSubjectX500Principal(); LdapName dn; try { // getName() provides RFC2253-encoded data. dn = new LdapName(principal.getName()); } catch (InvalidNameException e) { // Getting here may represent a bug in the standard libraries. LOGGER.log(Level.WARNING, "Denying caller: non-parsable Subject"); return false; } String commonName = null; for (Rdn rdn : dn.getRdns()) { if ("CN".equalsIgnoreCase(rdn.getType()) && (rdn.getValue() instanceof String)) { commonName = (String) rdn.getValue(); break; } } if (commonName == null) { LOGGER.log(Level.WARNING, "Denying caller: could not get Common Name"); return false; } commonName = commonName.toLowerCase(Locale.ENGLISH); boolean allowed = allowedAccessCommonNames.contains(commonName); if (!allowed) { LOGGER.log(Level.WARNING, "Denying caller: {0}", commonName); } return allowed; } } }