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}