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}