RequestFilter.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.catalina.filters;


import java.io.IOException;
import java.util.regex.Pattern;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * Implementation of a Filter that performs filtering based on comparing the appropriate request property (selected
 * based on which subclass you choose to configure into your Container's pipeline) against the regular expressions
 * configured for this Filter.
 * <p>
 * This filter is configured by setting the <code>allow</code> and/or <code>deny</code> properties to a regular
 * expressions (in the syntax supported by {@link Pattern}) to which the appropriate request property will be compared.
 * Evaluation proceeds as follows:
 * <ul>
 * <li>The subclass extracts the request property to be filtered, and calls the common <code>process()</code> method.
 * <li>If there is a deny expression configured, the property will be compared to the expression. If a match is found,
 * this request will be rejected with a "Forbidden" HTTP response.</li>
 * <li>If there is a allow expression configured, the property will be compared to the expression. If a match is found,
 * this request will be allowed to pass through to the next filter in the current pipeline.</li>
 * <li>If a deny expression was specified but no allow expression, allow this request to pass through (because none of
 * the deny expressions matched it).
 * <li>The request will be rejected with a "Forbidden" HTTP response.</li>
 * </ul>
 */
public abstract class RequestFilter extends FilterBase {


    // ----------------------------------------------------- Instance Variables

    /**
     * The regular expression used to test for allowed requests.
     */
    protected Pattern allow = null;

    /**
     * The regular expression used to test for denied requests.
     */
    protected Pattern deny = null;

    /**
     * The HTTP response status code that is used when rejecting denied request. It is 403 by default, but may be
     * changed to be 404.
     */
    protected int denyStatus = HttpServletResponse.SC_FORBIDDEN;

    /**
     * mime type -- "text/plain"
     */
    private static final String PLAIN_TEXT_MIME_TYPE = "text/plain";


    // ------------------------------------------------------------- Properties


    /**
     * @return the regular expression used to test for allowed requests for this Filter, if any; otherwise, return
     *             <code>null</code>.
     */
    public String getAllow() {
        if (allow == null) {
            return null;
        }
        return allow.toString();
    }


    /**
     * Set the regular expression used to test for allowed requests for this Filter, if any.
     *
     * @param allow The new allow expression
     */
    public void setAllow(String allow) {
        if (allow == null || allow.length() == 0) {
            this.allow = null;
        } else {
            this.allow = Pattern.compile(allow);
        }
    }


    /**
     * @return the regular expression used to test for denied requests for this Filter, if any; otherwise, return
     *             <code>null</code>.
     */
    public String getDeny() {
        if (deny == null) {
            return null;
        }
        return deny.toString();
    }


    /**
     * Set the regular expression used to test for denied requests for this Filter, if any.
     *
     * @param deny The new deny expression
     */
    public void setDeny(String deny) {
        if (deny == null || deny.length() == 0) {
            this.deny = null;
        } else {
            this.deny = Pattern.compile(deny);
        }
    }


    /**
     * @return response status code that is used to reject denied request.
     */
    public int getDenyStatus() {
        return denyStatus;
    }


    /**
     * Set response status code that is used to reject denied request.
     *
     * @param denyStatus The status code for deny
     */
    public void setDenyStatus(int denyStatus) {
        this.denyStatus = denyStatus;
    }


    // --------------------------------------------------------- Public Methods


    /**
     * Extract the desired request property, and pass it (along with the specified request and response objects) to the
     * protected <code>process()</code> method to perform the actual filtering. This method must be implemented by a
     * concrete subclass.
     *
     * @param request  The servlet request to be processed
     * @param response The servlet response to be created
     * @param chain    The filter chain
     *
     * @exception IOException      if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    @Override
    public abstract void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;


    // ------------------------------------------------------ Protected Methods


    @Override
    protected boolean isConfigProblemFatal() {
        return true;
    }


    /**
     * Perform the filtering that has been configured for this Filter, matching against the specified request property.
     *
     * @param property The request property on which to filter
     * @param request  The servlet request to be processed
     * @param response The servlet response to be processed
     * @param chain    The filter chain
     *
     * @exception IOException      if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    protected void process(String property, ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        if (isAllowed(property)) {
            chain.doFilter(request, response);
        } else {
            if (response instanceof HttpServletResponse) {
                if (getLogger().isDebugEnabled()) {
                    getLogger().debug(sm.getString("requestFilter.deny", ((HttpServletRequest) request).getRequestURI(),
                            property));
                }
                ((HttpServletResponse) response).sendError(denyStatus);
            } else {
                sendErrorWhenNotHttp(response);
            }
        }
    }


    /**
     * Process the allow and deny rules for the provided property.
     *
     * @param property The property to test against the allow and deny lists
     *
     * @return <code>true</code> if this request should be allowed, <code>false</code> otherwise
     */
    private boolean isAllowed(String property) {
        if (deny != null && deny.matcher(property).matches()) {
            return false;
        }

        // Check the allow patterns, if any
        if (allow != null && allow.matcher(property).matches()) {
            return true;
        }

        // Allow if denies specified but not allows
        if (deny != null && allow == null) {
            return true;
        }

        // Deny this request
        return false;
    }

    private void sendErrorWhenNotHttp(ServletResponse response) throws IOException {
        response.setContentType(PLAIN_TEXT_MIME_TYPE);
        response.getWriter().write(sm.getString("http.403"));
        response.getWriter().flush();
    }
}