001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.web; 021 022import com.google.common.collect.ImmutableList; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.HashSet; 027import java.util.LinkedHashSet; 028import java.util.List; 029import java.util.Set; 030import java.util.function.Predicate; 031import java.util.stream.Collectors; 032import javax.servlet.Filter; 033import org.sonar.api.ExtensionPoint; 034import org.sonar.api.server.ServerSide; 035 036import static com.google.common.base.Preconditions.checkArgument; 037import static java.util.Arrays.asList; 038import static java.util.Collections.unmodifiableList; 039 040/** 041 * @since 3.1 042 */ 043@ServerSide 044@ExtensionPoint 045public abstract class ServletFilter implements Filter { 046 047 /** 048 * Override to change URL. Default is /* 049 */ 050 public UrlPattern doGetPattern() { 051 return UrlPattern.builder().build(); 052 } 053 054 public static final class UrlPattern { 055 056 private static final String MATCH_ALL = "/*"; 057 058 private final List<String> inclusions; 059 private final List<String> exclusions; 060 private final Predicate<String>[] inclusionPredicates; 061 private final Predicate<String>[] exclusionPredicates; 062 063 private UrlPattern(Builder builder) { 064 this.inclusions = unmodifiableList(new ArrayList<>(builder.inclusions)); 065 this.exclusions = unmodifiableList(new ArrayList<>(builder.exclusions)); 066 if (builder.inclusionPredicates.isEmpty()) { 067 // because Stream#anyMatch() returns false if stream is empty 068 this.inclusionPredicates = new Predicate[] {s -> true}; 069 } else { 070 this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new); 071 } 072 this.exclusionPredicates = builder.exclusionPredicates.stream().toArray(Predicate[]::new); 073 } 074 075 public boolean matches(String path) { 076 return !Arrays.stream(exclusionPredicates).anyMatch(pattern -> pattern.test(path)) && 077 Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path)); 078 } 079 080 /** 081 * @since 6.0 082 */ 083 public Collection<String> getInclusions() { 084 return inclusions; 085 } 086 087 /** 088 * @since 6.0 089 */ 090 public Collection<String> getExclusions() { 091 return exclusions; 092 } 093 094 /** 095 * @deprecated replaced in version 6.0 by {@link #getInclusions()} and {@link #getExclusions()} 096 * @throws IllegalStateException if at least one exclusion or more than one inclusions are defined 097 */ 098 @Deprecated 099 public String getUrl() { 100 // Before 6.0, it was only possible to include one url 101 if (exclusions.isEmpty() && inclusions.size() == 1) { 102 return inclusions.get(0); 103 } 104 throw new IllegalStateException("this method is deprecated and should not be used anymore"); 105 } 106 107 public String label() { 108 return "UrlPattern{" + 109 "inclusions=[" + convertPatternsToString(inclusions) + "]" + 110 ", exclusions=[" + convertPatternsToString(exclusions) + "]" + 111 '}'; 112 } 113 114 private static String convertPatternsToString(List<String> input) { 115 StringBuilder output = new StringBuilder(); 116 if (input.isEmpty()) { 117 return ""; 118 } 119 if (input.size() == 1) { 120 return output.append(input.get(0)).toString(); 121 } 122 return output.append(input.get(0)).append(", ...").toString(); 123 } 124 125 /** 126 * Defines only a single inclusion pattern. This is a shortcut for {@code builder().includes(inclusionPattern).build()}. 127 */ 128 public static UrlPattern create(String inclusionPattern) { 129 return builder().includes(inclusionPattern).build(); 130 } 131 132 /** 133 * @since 6.0 134 */ 135 public static Builder builder() { 136 return new Builder(); 137 } 138 139 /** 140 * @since 6.0 141 */ 142 public static class Builder { 143 private static final String WILDCARD_CHAR = "*"; 144 private static final Collection<String> STATIC_RESOURCES = ImmutableList.of("/css/*", "/fonts/*", "/images/*", "/js/*", "/static/*", 145 "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"); 146 147 private final Set<String> inclusions = new LinkedHashSet<>(); 148 private final Set<String> exclusions = new LinkedHashSet<>(); 149 private final Set<Predicate<String>> inclusionPredicates = new HashSet<>(); 150 private final Set<Predicate<String>> exclusionPredicates = new HashSet<>(); 151 152 private Builder() { 153 } 154 155 public static Collection<String> staticResourcePatterns() { 156 return STATIC_RESOURCES; 157 } 158 159 /** 160 * Add inclusion patterns. Supported formats are: 161 * <ul> 162 * <li>path prefixed by / and ended by *, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"</li> 163 * <li>path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"</li> 164 * <li>path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"</li> 165 * </ul> 166 */ 167 public Builder includes(String... includePatterns) { 168 return includes(asList(includePatterns)); 169 } 170 171 /** 172 * Add exclusion patterns. See format described in {@link #includes(String...)} 173 */ 174 public Builder includes(Collection<String> includePatterns) { 175 this.inclusions.addAll(includePatterns); 176 this.inclusionPredicates.addAll(includePatterns.stream() 177 .filter(pattern -> !MATCH_ALL.equals(pattern)) 178 .map(Builder::compile) 179 .collect(Collectors.toList())); 180 return this; 181 } 182 183 public Builder excludes(String... excludePatterns) { 184 return excludes(asList(excludePatterns)); 185 } 186 187 public Builder excludes(Collection<String> excludePatterns) { 188 this.exclusions.addAll(excludePatterns); 189 this.exclusionPredicates.addAll(excludePatterns.stream() 190 .map(Builder::compile) 191 .collect(Collectors.toList())); 192 return this; 193 } 194 195 public UrlPattern build() { 196 return new UrlPattern(this); 197 } 198 199 private static Predicate<String> compile(String pattern) { 200 int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length(); 201 if (countStars == 0) { 202 checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern); 203 return url -> url.equals(pattern); 204 } 205 checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern); 206 if (pattern.charAt(0) == '/') { 207 checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern); 208 // remove the ending /* or * 209 String path = pattern.replaceAll("/?\\*", ""); 210 return url -> url.startsWith(path); 211 } 212 checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern); 213 // remove the leading * 214 String path = pattern.substring(1); 215 return url -> url.endsWith(path); 216 } 217 } 218 } 219}