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}