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 /** 106 * Defines only a single inclusion pattern. This is a shortcut for {@code builder().includes(inclusionPattern).build()}. 107 */ 108 public static UrlPattern create(String inclusionPattern) { 109 return builder().includes(inclusionPattern).build(); 110 } 111 112 /** 113 * @since 6.0 114 */ 115 public static Builder builder() { 116 return new Builder(); 117 } 118 119 /** 120 * @since 6.0 121 */ 122 public static class Builder { 123 private static final String WILDCARD_CHAR = "*"; 124 private static final Collection<String> STATIC_RESOURCES = ImmutableList.of("/css/*", "/fonts/*", "/images/*", "/js/*", "/static/*", 125 "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"); 126 127 private final Set<String> inclusions = new LinkedHashSet<>(); 128 private final Set<String> exclusions = new LinkedHashSet<>(); 129 private final Set<Predicate<String>> inclusionPredicates = new HashSet<>(); 130 private final Set<Predicate<String>> exclusionPredicates = new HashSet<>(); 131 132 private Builder() { 133 } 134 135 public static Collection<String> staticResourcePatterns() { 136 return STATIC_RESOURCES; 137 } 138 139 /** 140 * Add inclusion patterns. Supported formats are: 141 * <ul> 142 * <li>path prefixed by / and ended by *, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"</li> 143 * <li>path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"</li> 144 * <li>path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"</li> 145 * </ul> 146 */ 147 public Builder includes(String... includePatterns) { 148 return includes(asList(includePatterns)); 149 } 150 151 /** 152 * Add exclusion patterns. See format described in {@link #includes(String...)} 153 */ 154 public Builder includes(Collection<String> includePatterns) { 155 this.inclusions.addAll(includePatterns); 156 this.inclusionPredicates.addAll(includePatterns.stream() 157 .filter(pattern -> !MATCH_ALL.equals(pattern)) 158 .map(Builder::compile) 159 .collect(Collectors.toList())); 160 return this; 161 } 162 163 public Builder excludes(String... excludePatterns) { 164 return excludes(asList(excludePatterns)); 165 } 166 167 public Builder excludes(Collection<String> excludePatterns) { 168 this.exclusions.addAll(excludePatterns); 169 this.exclusionPredicates.addAll(excludePatterns.stream() 170 .map(Builder::compile) 171 .collect(Collectors.toList())); 172 return this; 173 } 174 175 public UrlPattern build() { 176 return new UrlPattern(this); 177 } 178 179 private static Predicate<String> compile(String pattern) { 180 int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length(); 181 if (countStars == 0) { 182 checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern); 183 return url -> url.equals(pattern); 184 } 185 checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern); 186 if (pattern.charAt(0) == '/') { 187 checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern); 188 // remove the ending /* or * 189 String path = pattern.replaceAll("/?\\*", ""); 190 return url -> url.startsWith(path); 191 } 192 checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern); 193 // remove the leading * 194 String path = pattern.substring(1); 195 return url -> url.endsWith(path); 196 } 197 } 198 } 199}