/* * 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.rewrite; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import org.apache.catalina.util.URLEncoder; public class Substitution { public abstract class SubstitutionElement { public abstract String evaluate(Matcher rule, Matcher cond, Resolver resolver); } public class StaticElement extends SubstitutionElement { public String value; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { return value; } } public class RewriteRuleBackReferenceElement extends SubstitutionElement { public int n; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { String result = rule.group(n); if (result == null) { result = ""; } if (escapeBackReferences) { // Note: This should be consistent with the way httpd behaves. // We might want to consider providing a dedicated decoder // with an option to add additional safe characters to // provide users with more flexibility return URLEncoder.DEFAULT.encode(result, resolver.getUriCharset()); } else { return result; } } } public class RewriteCondBackReferenceElement extends SubstitutionElement { public int n; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { return (cond.group(n) == null ? "" : cond.group(n)); } } public class ServerVariableElement extends SubstitutionElement { public String key; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { return resolver.resolve(key); } } public class ServerVariableEnvElement extends SubstitutionElement { public String key; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { return resolver.resolveEnv(key); } } public class ServerVariableSslElement extends SubstitutionElement { public String key; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { return resolver.resolveSsl(key); } } public class ServerVariableHttpElement extends SubstitutionElement { public String key; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { return resolver.resolveHttp(key); } } public class MapElement extends SubstitutionElement { public RewriteMap map = null; public String key; public String defaultValue = ""; public int n; @Override public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { String result = map.lookup(rule.group(n)); if (result == null) { result = defaultValue; } return result; } } protected SubstitutionElement[] elements = null; protected String sub = null; public String getSub() { return sub; } public void setSub(String sub) { this.sub = sub; } private boolean escapeBackReferences; void setEscapeBackReferences(boolean escapeBackReferences) { this.escapeBackReferences = escapeBackReferences; } public void parse(Map<String, RewriteMap> maps) { List<SubstitutionElement> elements = new ArrayList<>(); int pos = 0; int percentPos = 0; int dollarPos = 0; int backslashPos = 0; while (pos < sub.length()) { percentPos = sub.indexOf('%', pos); dollarPos = sub.indexOf('$', pos); backslashPos = sub.indexOf('\\', pos); if (percentPos == -1 && dollarPos == -1 && backslashPos == -1) { // Static text StaticElement newElement = new StaticElement(); newElement.value = sub.substring(pos, sub.length()); pos = sub.length(); elements.add(newElement); } else if (isFirstPos(backslashPos, dollarPos, percentPos)) { if (backslashPos + 1 == sub.length()) { throw new IllegalArgumentException(sub); } StaticElement newElement = new StaticElement(); newElement.value = sub.substring(pos, backslashPos) + sub.substring(backslashPos + 1, backslashPos + 2); pos = backslashPos + 2; elements.add(newElement); } else if (isFirstPos(dollarPos, percentPos)) { // $: back reference to rule or map lookup if (dollarPos + 1 == sub.length()) { throw new IllegalArgumentException(sub); } if (pos < dollarPos) { // Static text StaticElement newElement = new StaticElement(); newElement.value = sub.substring(pos, dollarPos); pos = dollarPos; elements.add(newElement); } if (Character.isDigit(sub.charAt(dollarPos + 1))) { // $: back reference to rule RewriteRuleBackReferenceElement newElement = new RewriteRuleBackReferenceElement(); newElement.n = Character.digit(sub.charAt(dollarPos + 1), 10); pos = dollarPos + 2; elements.add(newElement); } else if (sub.charAt(dollarPos + 1) == '{') { // $: map lookup as ${mapname:key|default} MapElement newElement = new MapElement(); int open = sub.indexOf('{', dollarPos); int colon = sub.indexOf(':', dollarPos); int def = sub.indexOf('|', dollarPos); int close = sub.indexOf('}', dollarPos); if (!(-1 < open && open < colon && colon < close)) { throw new IllegalArgumentException(sub); } newElement.map = maps.get(sub.substring(open + 1, colon)); if (newElement.map == null) { throw new IllegalArgumentException(sub + ": No map: " + sub.substring(open + 1, colon)); } if (def > -1) { if (!(colon < def && def < close)) { throw new IllegalArgumentException(sub); } newElement.key = sub.substring(colon + 1, def); newElement.defaultValue = sub.substring(def + 1, close); } else { newElement.key = sub.substring(colon + 1, close); } if (newElement.key.startsWith("$")) { newElement.n = Integer.parseInt(newElement.key.substring(1)); } pos = close + 1; elements.add(newElement); } else { throw new IllegalArgumentException(sub + ": missing digit or curly brace."); } } else { // %: back reference to condition or server variable if (percentPos + 1 == sub.length()) { throw new IllegalArgumentException(sub); } if (pos < percentPos) { // Static text StaticElement newElement = new StaticElement(); newElement.value = sub.substring(pos, percentPos); pos = percentPos; elements.add(newElement); } if (Character.isDigit(sub.charAt(percentPos + 1))) { // %: back reference to condition RewriteCondBackReferenceElement newElement = new RewriteCondBackReferenceElement(); newElement.n = Character.digit(sub.charAt(percentPos + 1), 10); pos = percentPos + 2; elements.add(newElement); } else if (sub.charAt(percentPos + 1) == '{') { // %: server variable as %{variable} SubstitutionElement newElement = null; int open = sub.indexOf('{', percentPos); int colon = sub.indexOf(':', percentPos); int close = sub.indexOf('}', percentPos); if (!(-1 < open && open < close)) { throw new IllegalArgumentException(sub); } if (colon > -1 && open < colon && colon < close) { String type = sub.substring(open + 1, colon); if (type.equals("ENV")) { newElement = new ServerVariableEnvElement(); ((ServerVariableEnvElement) newElement).key = sub.substring(colon + 1, close); } else if (type.equals("SSL")) { newElement = new ServerVariableSslElement(); ((ServerVariableSslElement) newElement).key = sub.substring(colon + 1, close); } else if (type.equals("HTTP")) { newElement = new ServerVariableHttpElement(); ((ServerVariableHttpElement) newElement).key = sub.substring(colon + 1, close); } else { throw new IllegalArgumentException(sub + ": Bad type: " + type); } } else { newElement = new ServerVariableElement(); ((ServerVariableElement) newElement).key = sub.substring(open + 1, close); } pos = close + 1; elements.add(newElement); } else { throw new IllegalArgumentException(sub + ": missing digit or curly brace."); } } } this.elements = elements.toArray(new SubstitutionElement[0]); } /** * Evaluate the substitution based on the context. * @param rule corresponding matched rule * @param cond last matched condition * @param resolver The property resolver * @return The substitution result */ public String evaluate(Matcher rule, Matcher cond, Resolver resolver) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < elements.length; i++) { buf.append(elements[i].evaluate(rule, cond, resolver)); } return buf.toString(); } /** * Checks whether the first int is non negative and smaller than any non negative other int * given with {@code others}. * * @param testPos * integer to test against * @param others * list of integers that are paired against {@code testPos}. Any * negative integer will be ignored. * @return {@code true} if {@code testPos} is not negative and is less then any given other * integer, {@code false} otherwise */ private boolean isFirstPos(int testPos, int... others) { if (testPos < 0) { return false; } for (int other : others) { if (other >= 0 && other < testPos) { return false; } } return true; } }