diff --git a/java/org/apache/catalina/filters/RemoteNetmaskFilter.java b/java/org/apache/catalina/filters/RemoteNetmaskFilter.java new file mode 100644 index 0000000..b6e40a4 --- /dev/null +++ b/java/org/apache/catalina/filters/RemoteNetmaskFilter.java @@ -0,0 +1,97 @@ +/* + * 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 org.apache.catalina.comet.CometEvent; +import org.apache.catalina.comet.CometFilterChain; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + + +/** + * Concrete implementation of RequestNetmaskFilter that filters + * based on the string representation of the remote client's IP address. + * + */ + +public final class RemoteNetmaskFilter + extends RequestFilter { + + // ----------------------------------------------------- Instance Variables + private static final Log log = LogFactory.getLog(RemoteNetmaskFilter.class); + + + // ------------------------------------------------------------- Properties + + + + // --------------------------------------------------------- Public Methods + + + /** + * Extract the desired request property, and pass it (along with the + * specified request and response objects and associated filter chain) to + * the protected process() method to perform the actual + * filtering. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * @param chain The filter chain for this request + * + * @exception java.io.IOException if an input/output error occurs + * @exception javax.servlet.ServletException if a servlet error occurs + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + process(request.getRemoteAddr(), request, response, chain); + + } + + /** + * Extract the desired request property, and pass it (along with the comet + * event and filter chain) to the protected process() method + * to perform the actual filtering. + * + * @param event The comet event to be processed + * @param chain The filter chain for this event + * + * @exception java.io.IOException if an input/output error occurs + * @exception javax.servlet.ServletException if a servlet error occurs + */ + @Override + public void doFilterEvent(CometEvent event, CometFilterChain chain) + throws IOException, ServletException { + processCometEvent(event.getHttpServletRequest().getRemoteHost(), + event, chain); + } + + @Override + protected Log getLogger() { + return log; + } +} diff --git a/java/org/apache/catalina/filters/RequestNetmaskFilter.java b/java/org/apache/catalina/filters/RequestNetmaskFilter.java new file mode 100644 index 0000000..25088dd --- /dev/null +++ b/java/org/apache/catalina/filters/RequestNetmaskFilter.java @@ -0,0 +1,269 @@ +/* + * 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 org.apache.catalina.comet.CometEvent; +import org.apache.catalina.comet.CometFilter; +import org.apache.catalina.comet.CometFilterChain; +import org.apache.catalina.util.NetMask; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +/** + * 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. + *

+ * This filter is configured by setting the allow and/or + * deny properties to a regular expressions (in the syntax + * supported by {@link java.util.regex.Pattern}) to which the appropriate request property will + * be compared. Evaluation proceeds as follows: + *

+ */ + +public abstract class RequestNetmaskFilter + extends FilterBase implements CometFilter { + + + // ----------------------------------------------------- Instance Variables + + /** + * The regular expression used to test for allowed requests. + */ + protected final List allow = new ArrayList(); + + /** + * The regular expression used to test for denied requests. + */ + protected final List deny = new ArrayList(); + + /** + * mime type -- "text/plain" + */ + private static final String PLAIN_TEXT_MIME_TYPE = "text/plain"; + + + // ------------------------------------------------------------- Properties + + + /** + * Return a string representation of the NetMask list in allow. + */ + public String getAllow() { + return allow.toString(); + } + + + /** + * Fill the allow list with the list of netmasks provided as an argument, + * if any. + * + * @param input The list of netmasks, as a comma separated string + */ + public void setAllow(final String input) { + if (input == null || input.length() == 0) + return; + + NetMask nm; + + for (final String s: input.split("\\s*,\\s*")) + try { + nm = new NetMask(s); + allow.add(nm); + } catch (IllegalArgumentException e) { + // FIXME: log + } + } + + + /** + * Return a string representation of the NetMask list in deny. + */ + public String getDeny() { + if (deny == null) { + return null; + } + return deny.toString(); + } + + + /** + * Fill the deny list with the list of netmasks provided as an argument, + * if any. + * + * @param input The list of netmasks, as a comma separated string + */ + public void setDeny(String input) { + if (input == null || input.length() == 0) + return; + + NetMask nm; + + for (final String s: input.split("\\s*,\\s*")) + try { + nm = new NetMask(s); + deny.add(nm); + } catch (IllegalArgumentException e) { + // FIXME: log + } + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Extract the desired request property, and pass it (along with the + * specified request and response objects) to the protected + * process() 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 java.io.IOException if an input/output error occurs + * @exception javax.servlet.ServletException if a servlet error occurs + */ + @Override + public abstract void doFilter(ServletRequest request, + ServletResponse response, FilterChain chain) throws IOException, + ServletException; + + + // ------------------------------------------------------ Protected Methods + + + /** + * 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 java.io.IOException if an input/output error occurs + * @exception javax.servlet.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); + return; + } + + if (!(response instanceof HttpServletResponse)) { + sendErrorWhenNotHttp(response); + return; + } + + ((HttpServletResponse) response) + .sendError(HttpServletResponse.SC_FORBIDDEN); + } + + /** + * Perform the filtering that has been configured for this Filter, matching + * against the specified request property. + * + * @param property The property to check against the allow/deny rules + * @param event The comet event to be filtered + * @param chain The comet filter chain + * @exception java.io.IOException if an input/output error occurs + * @exception javax.servlet.ServletException if a servlet error occurs + */ + protected void processCometEvent(String property, CometEvent event, + CometFilterChain chain) throws IOException, ServletException { + HttpServletResponse response = event.getHttpServletResponse(); + + if (isAllowed(property)) { + chain.doFilterEvent(event); + return; + } + + response.sendError(HttpServletResponse.SC_FORBIDDEN); + event.close(); + } + + /** + * Process the allow and deny rules for the provided property. + * + * @param property The property to test against the allow and deny lists + * @return true if this request should be allowed, + * false otherwise + */ + private boolean isAllowed(String property) { + final InetAddress addr; + + try { + addr = InetAddress.getByName(property); + } catch (UnknownHostException e) { + //Eh? + return false; + } + + for (final NetMask nm: deny) + if (nm.matches(addr)) + return false; + + for (final NetMask nm: allow) + if (nm.matches(addr)) + return true; + + // Allow if denies specified but not allows + if (!deny.isEmpty() && allow.isEmpty()) + 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(); + } +} diff --git a/java/org/apache/catalina/util/NetMask.java b/java/org/apache/catalina/util/NetMask.java new file mode 100644 index 0000000..b834443 --- /dev/null +++ b/java/org/apache/catalina/util/NetMask.java @@ -0,0 +1,103 @@ +package org.apache.catalina.util; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * A class representing a netmask, which is at the core of this valve. + * + *

The constructor takes a {@link java.lang.String} representing a + * CIDR netmask as an argument and extracts two informations from it: the + * network address and the CIDR. It then turns the address into a {@link + * java.math.BigInteger}, calculates the right shift and shifts that + * BigInteger by it.

+ *

The process to verify whether an IP address falls within the mask + * is to also convert it to a BigInteger, shifting it right and comparing + * it to the stored BigInteger. + *

+ */ + +public final class NetMask { + /** + * The argument to the constructor, used for .toString() + */ + private final String expression; + + /** + * The number of bits a matching candidate needs to be shifted right + * in order to see if it matches + */ + private final int shift; + + /** + * The network address, already shifted right + */ + private final BigInteger mask; + + /** + * Constructor. + * + * @param expression the CIDR netmask + * @throws IllegalArgumentException if the netmask is not correct + * (invalid address specification, malformed CIDR prefix, etc) + */ + public NetMask(final String expression) { + final int idx = expression.indexOf("/"); + final int cidr, addrlen; + final String addressPart; + final InetAddress addr; + final byte[] bytes; + + if (idx == -1) { + cidr = -1; + addressPart = expression; + } else { + final String substring = expression.substring(idx + 1); + try { + cidr = Integer.parseInt(substring); + if (cidr < 0) + throw new NumberFormatException("CIDR is negative"); + } catch (NumberFormatException ignored) { + throw new IllegalArgumentException("provided CIDR mask (" + + substring + ") is invalid"); + } + addressPart = expression.substring(0, idx); + } + + try { + addr = InetAddress.getByName(addressPart); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("provided address (" + + addressPart + ") is invalid"); + } + + bytes = addr.getAddress(); + addrlen = bytes.length * 8; + shift = cidr == -1 ? 0 : addrlen - cidr; + + if (shift < 0) + throw new IllegalArgumentException("CIDR prefix (" + cidr + + ") is greater than address length (" + addrlen + ")"); + mask = new BigInteger(bytes).shiftRight(shift); + this.expression = expression; + } + + /** + * Test if a given address matches this netmask + * + * @param addr The {@link java.net.InetAddress} to test + * @return true on match, false otherwise + */ + public boolean matches (final InetAddress addr) { + final BigInteger provided = new BigInteger(addr.getAddress()) + .shiftRight(shift); + + return mask.equals(provided); + } + + @Override + public String toString() { + return expression; + } +} diff --git a/java/org/apache/catalina/valves/RemoteNetmaskValve.java b/java/org/apache/catalina/valves/RemoteNetmaskValve.java new file mode 100644 index 0000000..af21f84 --- /dev/null +++ b/java/org/apache/catalina/valves/RemoteNetmaskValve.java @@ -0,0 +1,86 @@ +/* + * 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.valves; + + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; + +import javax.servlet.ServletException; +import java.io.IOException; + + +/** + * Concrete implementation of RequestFilterValve that filters + * based on the string representation of the remote client's IP address. + * + * @author Craig R. McClanahan + * @version $Id$ + */ + +public final class RemoteNetmaskValve + extends RequestNetmaskValve +{ + + + // ----------------------------------------------------- Instance Variables + + + /** + * The descriptive information related to this implementation. + */ + private static final String info = + "org.apache.catalina.valves.RemoteNetmaskValve/1.0"; + + + // ------------------------------------------------------------- Properties + + + /** + * Return descriptive information about this Valve implementation. + */ + @Override + public String getInfo() { + return (info); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Extract the desired request property, and pass it (along with the + * specified request and response objects) to the protected + * process() 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 + * + * @exception java.io.IOException if an input/output error occurs + * @exception javax.servlet.ServletException if a servlet error occurs + */ + @Override + public void invoke(Request request, Response response) + throws IOException, ServletException { + process(request.getRequest().getRemoteAddr(), request, response); + } + + +} diff --git a/java/org/apache/catalina/valves/RequestNetmaskValve.java b/java/org/apache/catalina/valves/RequestNetmaskValve.java new file mode 100644 index 0000000..6689c0a --- /dev/null +++ b/java/org/apache/catalina/valves/RequestNetmaskValve.java @@ -0,0 +1,220 @@ +/* + * 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.valves; + + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.NetMask; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Implementation of a Valve that performs IP filtering of a remote host + * based on the remote's IP address. This valve replicates (some of) Apache's + * "Order", "Allow from" and "Deny" directives with a few limitations: + *
    + *
  • "Order" will always be "deny, allow";
  • + *
  • the netmasks in the "allow" and "deny" properties must be fully + * qualified CIDR netmasks, and the CIDR must be an integer + * (expressions like "10.", "13.0.0.0/255.255.192.0" or ".foo.bar" will + * not work);
  • + *
  • as a side effect of using @{link java.net.InetAddress}, + * expressions like "my.host.com/27" will work, + * however their use is discouraged.
  • + *
+ *

Note that IPv6 is supported. Also note that invalid netmasks will be + * ignored.

+ *

+ * This Valve may be attached to any Container, depending on the granularity + * of the filtering you wish to perform.

+ * + * @author Francis Galiegue + */ + +public abstract class RequestNetmaskValve + extends ValveBase { + + //------------------------------------------------------ Constructor + public RequestNetmaskValve() { + super(true); + } + + // ----------------------------------------------------- Class Variables + + + /** + * The descriptive information related to this implementation. + */ + private static final String info = + "org.apache.catalina.valves.RequestNetmaskValve/1.0"; + + // ----------------------------------------------------- Instance Variables + + /** + * List of allowed netmasks, if any + */ + + protected final List allow = new ArrayList(); + + /** + * List of denied netmasks, if any + */ + + protected final List deny = new ArrayList(); + + // ------------------------------------------------------------- Properties + + + /** + * Return the allowed netmask list as a string. + */ + public String getAllow() { + return allow.toString(); + } + + + /** + * Fills the allowed Netmask list. + * + * @param input The input expression + */ + public void setAllow(final String input) { + if (input == null || input.length() == 0) + return; + + NetMask nm; + + for (final String s: input.split("\\s*,\\s*")) + try { + nm = new NetMask(s); + this.allow.add(nm); + } catch (IllegalArgumentException e) { + // FIXME: log + } + } + + + /** + * Return the denied netmask list as a string. + */ + public String getDeny() { + return deny.toString(); + } + + + /** + * Set the regular expression used to test for denied requests for this + * Valve, if any. + * + * @param input The new input expression + */ + public void setDeny(final String input) { + if (input == null || input.length() == 0) + return; + + NetMask nm; + + for (final String s: input.split("\\s*,\\s*")) + try { + nm = new NetMask(s); + this.deny.add(nm); + } catch (IllegalArgumentException e) { + // FIXME: log + } + } + + + /** + * Return descriptive information about this Valve implementation. + */ + @Override + public String getInfo() { + return (info); + } + + + // --------------------------------------------------------- Public Methods + + + /** + * Extract the desired request property, and pass it (along with the + * specified request and response objects) to the protected + * process() 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 + * + * @exception java.io.IOException if an input/output error occurs + * @exception javax.servlet.ServletException if a servlet error occurs + */ + @Override + public abstract void invoke(Request request, Response response) + throws IOException, ServletException; + + + // ------------------------------------------------------ Protected Methods + + + /** + * Perform the filtering that has been configured for this Valve, 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 + * + * @exception java.io.IOException if an input/output error occurs + * @exception javax.servlet.ServletException if a servlet error occurs + */ + protected void process(String property, Request request, Response response) + throws IOException, ServletException { + + final InetAddress addr = InetAddress.getByName(property); + + for (final NetMask nm: deny) + if (nm.matches(addr)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + if (allow.isEmpty()) { + getNext().invoke(request, response); + return; + } + + for (final NetMask nm: allow) + if (nm.matches(addr)) { + getNext().invoke(request, response); + return; + } + + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } +}