/** * Copyright 2016 Yahoo Inc. * * 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.yahoo.pulsar.broker.web; import java.io.IOException; 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.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import org.apache.zookeeper.KeeperException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.yahoo.pulsar.broker.PulsarService; import com.yahoo.pulsar.zookeeper.Deserializers; /** * Implementation of a servlet {@code Filter} which rejects requests from clients older than the configured minimum * version. * */ public class ApiVersionFilter implements Filter { // Needed constants. private static final String CLIENT_VERSION_PARAM = "pulsar-client-version"; private static final String MIN_API_VERSION_PATH = "/minApiVersion"; private static final Logger LOG = LoggerFactory.getLogger(ApiVersionFilter.class); /** * The PulsarService instance holding the Local ZooKeeper cache. We use this rather than the underlying Zk cache * directly as the cache is not initialized at the time the ApiVersionFilter is constructed. */ private final PulsarService pulsar; /** If true, clients which do not report a version will be allowed. */ private final boolean allowUnversionedClients; public ApiVersionFilter(PulsarService pulsar, boolean allowUnversionedClients) { this.pulsar = pulsar; this.allowUnversionedClients = allowUnversionedClients; } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { try { String minApiVersion = pulsar.getLocalZkCache().getData(MIN_API_VERSION_PATH, Deserializers.STRING_DESERIALIZER).orElseThrow(() -> new KeeperException.NoNodeException()); String requestApiVersion = getRequestApiVersion(req); if (shouldAllowRequest(req.getRemoteAddr(), minApiVersion, requestApiVersion)) { // Allow the request to continue by invoking the next filter in // the chain. chain.doFilter(req, resp); } else { // The client's API version is less than the min supported, // reject the request. HttpServletResponse httpResponse = (HttpServletResponse) resp; HttpServletResponseWrapper respWrapper = new HttpServletResponseWrapper(httpResponse); respWrapper.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unsuported Client version"); } } catch (Exception ex) { LOG.warn("[{}] Unable to safely determine client version eligibility. Allowing request", req.getRemoteAddr()); chain.doFilter(req, resp); } } @Override public void init(FilterConfig arg0) throws ServletException { // No init necessary. } @Override public void destroy() { // No state to clean up. } /** * Checks to see if {@code requestApiVersion} is greater than {@code minApiVersion}. Does that by converting both * {@code minApiVersion} and {@code requestApiVersion} to a floating point number. Assumes that version are properly * formatted floating point numbers. * * Note that this scheme implies that version numbers cannot be of the format x.y.z or any other format which is not * a valid floating point number. * * @param minApiVersion * @param requestApiVersion * @return true if requestApiVersion is greater than or equal to minApiVersion */ private boolean shouldAllowRequest(String clientAddress, String minApiVersion, String requestApiVersion) { if (requestApiVersion == null) { // The client has not sent a version, allow the request if // configured to do so. if (LOG.isDebugEnabled()) { LOG.debug("[{}] Checking client version: req: {} -- min: {} -- Allow unversioned: {}", clientAddress, requestApiVersion, minApiVersion, allowUnversionedClients); } return allowUnversionedClients; } try { float minVersion = Float.parseFloat(minApiVersion); float requestVersion = Float.parseFloat(requestApiVersion); if (LOG.isDebugEnabled()) { LOG.debug("[{}] Checking client version: req: {} -- min: {}", clientAddress, requestApiVersion, minApiVersion); } return minVersion <= requestVersion; } catch (NumberFormatException ex) { LOG.warn("[{}] Unable to convert version info to floats. " + "minVersion = {}, requestVersion = {}", clientAddress, minApiVersion, requestApiVersion); throw new IllegalArgumentException("Invalid Number in min or request API version"); } } private String getRequestApiVersion(ServletRequest req) { // Implementation assumes that the client version is in an HTTP header // named client_version. // TODO (agh) Ensure that this is the case. HttpServletRequest httpReq = (HttpServletRequest) req; return httpReq.getHeader(CLIENT_VERSION_PARAM); } }