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