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