diff --git a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java b/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java deleted file mode 100644 index 727e9a8160..0000000000 --- a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm; - -/** - * - * @author Sebastian Sdorra - * @since 1.17 - */ -public class ArgumentIsInvalidException extends IllegalStateException -{ - - /** - * Constructs ... - * - */ - public ArgumentIsInvalidException() - { - super(); - } - - /** - * Constructs ... - * - * - * @param s - */ - public ArgumentIsInvalidException(String s) - { - super(s); - } - - /** - * Constructs ... - * - * - * @param cause - */ - public ArgumentIsInvalidException(Throwable cause) - { - super(cause); - } - - /** - * Constructs ... - * - * - * @param message - * @param cause - */ - public ArgumentIsInvalidException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java index 6f6831b058..0797134c9f 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java @@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources; import com.google.common.collect.ImmutableList; import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.Arrays; @@ -14,7 +13,7 @@ import java.util.Arrays; * builder for each method. * *
- * LinkBuilder builder = new LinkBuilder(uriInfo, MainResource.class, SubResource.class);
+ * LinkBuilder builder = new LinkBuilder(pathInfo, MainResource.class, SubResource.class);
  * Link link = builder
  *     .method("sub")
  *     .parameters("param")
@@ -25,16 +24,16 @@ import java.util.Arrays;
  */
 @SuppressWarnings("WeakerAccess") // Non-public will result in IllegalAccessError for plugins
 public class LinkBuilder {
-  private final UriInfo uriInfo;
+  private final ScmPathInfo pathInfo;
   private final Class[] classes;
   private final ImmutableList calls;
 
-  public LinkBuilder(UriInfo uriInfo, Class... classes) {
-    this(uriInfo, classes, ImmutableList.of());
+  public LinkBuilder(ScmPathInfo pathInfo, Class... classes) {
+    this(pathInfo, classes, ImmutableList.of());
   }
 
-  private LinkBuilder(UriInfo uriInfo, Class[] classes, ImmutableList calls) {
-    this.uriInfo = uriInfo;
+  private LinkBuilder(ScmPathInfo pathInfo, Class[] classes, ImmutableList calls) {
+    this.pathInfo = pathInfo;
     this.classes = classes;
     this.calls = calls;
   }
@@ -51,7 +50,7 @@ public class LinkBuilder {
       throw new IllegalStateException("not enough methods for all classes");
     }
 
-    URI baseUri = uriInfo.getBaseUri();
+    URI baseUri = pathInfo.getApiRestUri();
     URI relativeUri = createRelativeUri();
     return baseUri.resolve(relativeUri);
   }
@@ -61,7 +60,7 @@ public class LinkBuilder {
   }
 
   private LinkBuilder add(String method, String[] parameters) {
-    return new LinkBuilder(uriInfo, classes, appendNewCall(method, parameters));
+    return new LinkBuilder(pathInfo, classes, appendNewCall(method, parameters));
   }
 
   private ImmutableList appendNewCall(String method, String[] parameters) {
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java
new file mode 100644
index 0000000000..fa975520c1
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java
@@ -0,0 +1,14 @@
+package sonia.scm.api.v2.resources;
+
+import java.net.URI;
+
+public interface ScmPathInfo {
+
+  String REST_API_PATH = "/api/rest";
+
+  URI getApiRestUri();
+
+  default URI getRootUri() {
+    return getApiRestUri().resolve("../..");
+  }
+}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java
new file mode 100644
index 0000000000..c88bd4a2b5
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java
@@ -0,0 +1,18 @@
+package sonia.scm.api.v2.resources;
+
+public class ScmPathInfoStore {
+
+  private ScmPathInfo pathInfo;
+
+  public ScmPathInfo get() {
+    return pathInfo;
+  }
+
+  public void set(ScmPathInfo info) {
+    if (this.pathInfo != null) {
+      throw new IllegalStateException("UriInfo already set");
+    }
+    this.pathInfo = info;
+  }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java
deleted file mode 100644
index 2f61383cfd..0000000000
--- a/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package sonia.scm.api.v2.resources;
-
-import javax.ws.rs.core.UriInfo;
-
-public class UriInfoStore {
-
-  private UriInfo uriInfo;
-
-  public UriInfo get() {
-    return uriInfo;
-  }
-
-  public void set(UriInfo uriInfo) {
-    if (this.uriInfo != null) {
-      throw new IllegalStateException("UriInfo already set");
-    }
-    this.uriInfo = uriInfo;
-  }
-}
diff --git a/scm-core/src/main/java/sonia/scm/filter/Filters.java b/scm-core/src/main/java/sonia/scm/filter/Filters.java
index b6a45811bc..b1f5ea47cf 100644
--- a/scm-core/src/main/java/sonia/scm/filter/Filters.java
+++ b/scm-core/src/main/java/sonia/scm/filter/Filters.java
@@ -31,6 +31,8 @@
 
 package sonia.scm.filter;
 
+import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
+
 /**
  * Useful constants for filter implementations.
  *
@@ -44,26 +46,26 @@ public final class Filters
   public static final String PATTERN_ALL = "/*";
 
   /** Field description */
-  public static final String PATTERN_CONFIG = "/api/rest/config*";
+  public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
 
   /** Field description */
   public static final String PATTERN_DEBUG = "/debug.html";
 
   /** Field description */
-  public static final String PATTERN_GROUPS = "/api/rest/groups*";
+  public static final String PATTERN_GROUPS = REST_API_PATH + "/groups*";
 
   /** Field description */
-  public static final String PATTERN_PLUGINS = "/api/rest/plugins*";
+  public static final String PATTERN_PLUGINS = REST_API_PATH + "/plugins*";
 
   /** Field description */
   public static final String PATTERN_RESOURCE_REGEX =
     "^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)";
 
   /** Field description */
-  public static final String PATTERN_RESTAPI = "/api/rest/*";
+  public static final String PATTERN_RESTAPI = REST_API_PATH + "/*";
 
   /** Field description */
-  public static final String PATTERN_USERS = "/api/rest/users*";
+  public static final String PATTERN_USERS = REST_API_PATH + "/users*";
 
   /** authentication priority */
   public static final int PRIORITY_AUTHENTICATION = 5000;
diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java
index 0d8c6a6af8..cad36f2d88 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Repository.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java
@@ -40,7 +40,6 @@ import com.google.common.base.Objects;
 import com.google.common.collect.Lists;
 import sonia.scm.BasicPropertiesAware;
 import sonia.scm.ModelObject;
-import sonia.scm.util.HttpUtil;
 import sonia.scm.util.Util;
 import sonia.scm.util.ValidationUtil;
 
@@ -349,17 +348,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
     // do not copy health check results
   }
 
-  /**
-   * Creates the url of the repository.
-   *
-   * @param baseUrl base url of the server including the context path
-   * @return url of the repository
-   * @since 1.17
-   */
-  public String createUrl(String baseUrl) {
-    return HttpUtil.concatenate(baseUrl, type, namespace, name);
-  }
-
   /**
    * Returns true if the {@link Repository} is the same as the obj argument.
    *
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
index 2c4d958d8d..1e2fdccf42 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java
@@ -38,7 +38,6 @@ package sonia.scm.repository;
 import sonia.scm.AlreadyExistsException;
 import sonia.scm.TypeManager;
 
-import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.util.Collection;
 
@@ -99,29 +98,6 @@ public interface RepositoryManager
    */
   public Collection getConfiguredTypes();
 
-  /**
-   * Returns the {@link Repository} associated to the request uri.
-   *
-   *
-   * @param request the current http request
-   *
-   * @return associated to the request uri
-   * @since 1.9
-   */
-  public Repository getFromRequest(HttpServletRequest request);
-
-  /**
-   * Returns the {@link Repository} associated to the request uri.
-   *
-   *
-   *
-   * @param uri request uri without context path
-   *
-   * @return  associated to the request uri
-   * @since 1.9
-   */
-  public Repository getFromUri(String uri);
-
   /**
    * Returns a {@link RepositoryHandler} by the given type (hg, git, svn ...).
    *
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
index aa4117af34..87df960da7 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java
@@ -39,7 +39,6 @@ import sonia.scm.AlreadyExistsException;
 import sonia.scm.ManagerDecorator;
 import sonia.scm.Type;
 
-import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.util.Collection;
 
@@ -120,34 +119,6 @@ public class RepositoryManagerDecorator
     return decorated;
   }
 
-  /**
-   * {@inheritDoc}
-   *
-   *
-   * @param request
-   *
-   * @return
-   */
-  @Override
-  public Repository getFromRequest(HttpServletRequest request)
-  {
-    return decorated.getFromRequest(request);
-  }
-
-  /**
-   * {@inheritDoc}
-   *
-   *
-   * @param uri
-   *
-   * @return
-   */
-  @Override
-  public Repository getFromUri(String uri)
-  {
-    return decorated.getFromUri(uri);
-  }
-
   /**
    * {@inheritDoc}
    *
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
index 070221117a..9dd866daa4 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java
@@ -44,8 +44,8 @@ import sonia.scm.NotFoundException;
 public class RepositoryNotFoundException extends NotFoundException
 {
 
-  /** Field description */
   private static final long serialVersionUID = -6583078808900520166L;
+  private static final String TYPE_REPOSITORY = "repository";
 
   //~--- constructors ---------------------------------------------------------
 
@@ -55,10 +55,14 @@ public class RepositoryNotFoundException extends NotFoundException
    *
    */
   public RepositoryNotFoundException(Repository repository) {
-    super("repository", repository.getName() + "/"  + repository.getNamespace());
+    super(TYPE_REPOSITORY, repository.getName() + "/"  + repository.getNamespace());
   }
 
   public RepositoryNotFoundException(String repositoryId) {
-    super("repository", repositoryId);
+    super(TYPE_REPOSITORY, repositoryId);
+  }
+
+  public RepositoryNotFoundException(NamespaceAndName namespaceAndName) {
+    super(TYPE_REPOSITORY, namespaceAndName.toString());
   }
 }
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java
index 1a5300ad21..cea8574d14 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java
@@ -6,13 +6,13 @@
  * modification, are permitted provided that the following conditions are met:
  *
  * 1. Redistributions of source code must retain the above copyright notice,
- *    this list of conditions and the following disclaimer.
+ * this list of conditions and the following disclaimer.
  * 2. Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
  * 3. Neither the name of SCM-Manager; nor the names of its
- *    contributors may be used to endorse or promote products derived from this
- *    software without specific prior written permission.
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
  *
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
@@ -26,35 +26,21 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *
  * http://bitbucket.org/sdorra/scm-manager
- *
  */
 
 
-
 package sonia.scm.repository;
 
 //~--- non-JDK imports --------------------------------------------------------
 
 import com.google.inject.throwingproviders.CheckedProvider;
 
-import sonia.scm.security.ScmSecurityException;
-
 /**
  *
  * @author Sebastian Sdorra
  * @since 1.10
  */
-public interface RepositoryProvider extends CheckedProvider
-{
-
-  /**
-   * Method description
-   *
-   *
-   * @return
-   *
-   * @throws ScmSecurityException
-   */
+public interface RepositoryProvider extends CheckedProvider {
   @Override
-  public Repository get() throws ScmSecurityException;
+  Repository get();
 }
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
index 9eb7427a4f..bdd6e4b320 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java
@@ -31,6 +31,7 @@
 
 package sonia.scm.repository.api;
 
+import lombok.extern.slf4j.Slf4j;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import sonia.scm.cache.CacheManager;
@@ -42,6 +43,8 @@ import sonia.scm.repository.spi.RepositoryServiceProvider;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Stream;
 
 /**
  * From the {@link RepositoryService} it is possible to access all commands for
@@ -78,30 +81,32 @@ import java.io.IOException;
  * @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
  * @since 1.17
  */
+@Slf4j
 public final class RepositoryService implements Closeable {
-  private CacheManager cacheManager;
-  private PreProcessorUtil preProcessorUtil;
-  private RepositoryServiceProvider provider;
-  private Repository repository;
-  private static final Logger logger =
-    LoggerFactory.getLogger(RepositoryService.class);
+
+  private static final Logger logger = LoggerFactory.getLogger(RepositoryService.class);
+
+  private final CacheManager cacheManager;
+  private final PreProcessorUtil preProcessorUtil;
+  private final RepositoryServiceProvider provider;
+  private final Repository repository;
+  private final Set protocolProviders;
 
   /**
    * Constructs a new {@link RepositoryService}. This constructor should only
    * be called from the {@link RepositoryServiceFactory}.
-   *
-   * @param cacheManager     cache manager
+   *  @param cacheManager     cache manager
    * @param provider         implementation for {@link RepositoryServiceProvider}
    * @param repository       the repository
-   * @param preProcessorUtil
    */
   RepositoryService(CacheManager cacheManager,
-                    RepositoryServiceProvider provider, Repository repository,
-                    PreProcessorUtil preProcessorUtil) {
+    RepositoryServiceProvider provider, Repository repository,
+    PreProcessorUtil preProcessorUtil, Set protocolProviders) {
     this.cacheManager = cacheManager;
     this.provider = provider;
     this.repository = repository;
     this.preProcessorUtil = preProcessorUtil;
+    this.protocolProviders = protocolProviders;
   }
 
   /**
@@ -125,7 +130,7 @@ public final class RepositoryService implements Closeable {
     try {
       provider.close();
     } catch (IOException ex) {
-      logger.error("Could not close repository service provider", ex);
+      log.error("Could not close repository service provider", ex);
     }
   }
 
@@ -138,7 +143,7 @@ public final class RepositoryService implements Closeable {
    */
   public BlameCommandBuilder getBlameCommand() {
     logger.debug("create blame command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(),
       repository, preProcessorUtil);
@@ -153,7 +158,7 @@ public final class RepositoryService implements Closeable {
    */
   public BranchesCommandBuilder getBranchesCommand() {
     logger.debug("create branches command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new BranchesCommandBuilder(cacheManager,
       provider.getBranchesCommand(), repository);
@@ -168,7 +173,7 @@ public final class RepositoryService implements Closeable {
    */
   public BrowseCommandBuilder getBrowseCommand() {
     logger.debug("create browse command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(),
       repository, preProcessorUtil);
@@ -184,7 +189,7 @@ public final class RepositoryService implements Closeable {
    */
   public BundleCommandBuilder getBundleCommand() {
     logger.debug("create bundle command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new BundleCommandBuilder(provider.getBundleCommand(), repository);
   }
@@ -198,7 +203,7 @@ public final class RepositoryService implements Closeable {
    */
   public CatCommandBuilder getCatCommand() {
     logger.debug("create cat command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new CatCommandBuilder(provider.getCatCommand());
   }
@@ -213,7 +218,7 @@ public final class RepositoryService implements Closeable {
    */
   public DiffCommandBuilder getDiffCommand() {
     logger.debug("create diff command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new DiffCommandBuilder(provider.getDiffCommand());
   }
@@ -229,7 +234,7 @@ public final class RepositoryService implements Closeable {
    */
   public IncomingCommandBuilder getIncomingCommand() {
     logger.debug("create incoming command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new IncomingCommandBuilder(cacheManager,
       provider.getIncomingCommand(), repository, preProcessorUtil);
@@ -244,7 +249,7 @@ public final class RepositoryService implements Closeable {
    */
   public LogCommandBuilder getLogCommand() {
     logger.debug("create log command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new LogCommandBuilder(cacheManager, provider.getLogCommand(),
       repository, preProcessorUtil);
@@ -258,7 +263,7 @@ public final class RepositoryService implements Closeable {
    *                                      by the implementation of the repository service provider.
    */
   public ModificationsCommandBuilder getModificationsCommand() {
-    logger.debug("create modifications command for repository {}",repository.getNamespaceAndName());
+    logger.debug("create modifications command for repository {}", repository.getNamespaceAndName());
     return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil);
   }
 
@@ -272,7 +277,7 @@ public final class RepositoryService implements Closeable {
    */
   public OutgoingCommandBuilder getOutgoingCommand() {
     logger.debug("create outgoing command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new OutgoingCommandBuilder(cacheManager,
       provider.getOutgoingCommand(), repository, preProcessorUtil);
@@ -288,7 +293,7 @@ public final class RepositoryService implements Closeable {
    */
   public PullCommandBuilder getPullCommand() {
     logger.debug("create pull command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new PullCommandBuilder(provider.getPullCommand(), repository);
   }
@@ -303,7 +308,7 @@ public final class RepositoryService implements Closeable {
    */
   public PushCommandBuilder getPushCommand() {
     logger.debug("create push command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new PushCommandBuilder(provider.getPushCommand());
   }
@@ -326,7 +331,7 @@ public final class RepositoryService implements Closeable {
    */
   public TagsCommandBuilder getTagsCommand() {
     logger.debug("create tags command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(),
       repository);
@@ -342,7 +347,7 @@ public final class RepositoryService implements Closeable {
    */
   public UnbundleCommandBuilder getUnbundleCommand() {
     logger.debug("create unbundle command for repository {}",
-      repository.getName());
+      repository.getNamespaceAndName());
 
     return new UnbundleCommandBuilder(provider.getUnbundleCommand(),
       repository);
@@ -369,5 +374,20 @@ public final class RepositoryService implements Closeable {
     return provider.getSupportedFeatures().contains(feature);
   }
 
+  public  Stream getSupportedProtocols() {
+    return protocolProviders.stream()
+      .filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType()))
+      .map(this::createProviderInstanceForRepository);
+  }
 
+  private  T createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
+    return protocolProvider.get(repository);
+  }
+
+  public  T getProtocol(Class clazz) {
+    return this.getSupportedProtocols()
+      .filter(scmProtocol -> clazz.isAssignableFrom(scmProtocol.getClass()))
+      .findFirst()
+      .orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType())));
+  }
 }
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
index 627e57a797..fbb1ee6b58 100644
--- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
+++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java
@@ -137,13 +137,15 @@ public final class RepositoryServiceFactory
   @Inject
   public RepositoryServiceFactory(ScmConfiguration configuration,
     CacheManager cacheManager, RepositoryManager repositoryManager,
-    Set resolvers, PreProcessorUtil preProcessorUtil)
+    Set resolvers, PreProcessorUtil preProcessorUtil,
+    Set protocolProviders)
   {
     this.configuration = configuration;
     this.cacheManager = cacheManager;
     this.repositoryManager = repositoryManager;
     this.resolvers = resolvers;
     this.preProcessorUtil = preProcessorUtil;
+    this.protocolProviders = protocolProviders;
 
     ScmEventBus.getInstance().register(new CacheClearHook(cacheManager));
   }
@@ -208,9 +210,7 @@ public final class RepositoryServiceFactory
 
     if (repository == null)
     {
-      String msg = "could not find a repository with namespace/name " + namespaceAndName;
-
-      throw new RepositoryNotFoundException(msg);
+      throw new RepositoryNotFoundException(namespaceAndName);
     }
 
     return create(repository);
@@ -254,7 +254,7 @@ public final class RepositoryServiceFactory
         }
 
         service = new RepositoryService(cacheManager, provider, repository,
-          preProcessorUtil);
+          preProcessorUtil, protocolProviders);
 
         break;
       }
@@ -369,4 +369,6 @@ public final class RepositoryServiceFactory
 
   /** service resolvers */
   private final Set resolvers;
+
+  private Set protocolProviders;
 }
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java
new file mode 100644
index 0000000000..c987510491
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java
@@ -0,0 +1,19 @@
+package sonia.scm.repository.api;
+
+/**
+ * An ScmProtocol represents a concrete protocol provided by the SCM-Manager instance
+ * to interact with a repository depending on its type. There may be multiple protocols
+ * available for a repository type (eg. http and ssh).
+ */
+public interface ScmProtocol {
+
+  /**
+   * The type of the concrete protocol, eg. "http" or "ssh".
+   */
+  String getType();
+
+  /**
+   * The URL to access the repository providing this protocol.
+   */
+  String getUrl();
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java
new file mode 100644
index 0000000000..597826676d
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java
@@ -0,0 +1,12 @@
+package sonia.scm.repository.api;
+
+import sonia.scm.plugin.ExtensionPoint;
+import sonia.scm.repository.Repository;
+
+@ExtensionPoint(multi = true)
+public interface ScmProtocolProvider {
+
+  String getType();
+
+  T get(Repository repository);
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java
new file mode 100644
index 0000000000..b933abf559
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java
@@ -0,0 +1,38 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.api.ScmProtocol;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+
+public abstract class HttpScmProtocol implements ScmProtocol {
+
+  private final Repository repository;
+  private final String basePath;
+
+  public HttpScmProtocol(Repository repository, String basePath) {
+    this.repository = repository;
+    this.basePath = basePath;
+  }
+
+  @Override
+  public String getType() {
+    return "http";
+  }
+
+  @Override
+  public String getUrl() {
+      return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", repository.getNamespace(), repository.getName())).toASCIIString();
+  }
+
+  public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException {
+    serve(request, response, repository, config);
+  }
+
+  protected abstract void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException;
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java
new file mode 100644
index 0000000000..c1b7229036
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java
@@ -0,0 +1,91 @@
+package sonia.scm.repository.spi;
+
+import lombok.extern.slf4j.Slf4j;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+import sonia.scm.config.ScmConfiguration;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.api.ScmProtocolProvider;
+
+import javax.inject.Provider;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Optional;
+
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+
+@Slf4j
+public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolProvider {
+
+  private final Provider delegateProvider;
+  private final Provider pathInfoStore;
+  private final ScmConfiguration scmConfiguration;
+
+  private volatile boolean isInitialized = false;
+
+
+  protected InitializingHttpScmProtocolWrapper(Provider delegateProvider, Provider pathInfoStore, ScmConfiguration scmConfiguration) {
+    this.delegateProvider = delegateProvider;
+    this.pathInfoStore = pathInfoStore;
+    this.scmConfiguration = scmConfiguration;
+  }
+
+  protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException {
+    httpServlet.init(config);
+  }
+
+  @Override
+  public HttpScmProtocol get(Repository repository) {
+    if (!repository.getType().equals(getType())) {
+      throw new IllegalArgumentException(String.format("cannot handle repository with type %s with protocol for type %s", repository.getType(), getType()));
+    }
+    return new ProtocolWrapper(repository, computeBasePath());
+  }
+
+  private String computeBasePath() {
+    return getPathFromScmPathInfoIfAvailable().orElse(getPathFromConfiguration());
+  }
+
+  private Optional getPathFromScmPathInfoIfAvailable() {
+    try {
+      ScmPathInfoStore scmPathInfoStore = pathInfoStore.get();
+      if (scmPathInfoStore != null && scmPathInfoStore.get() != null) {
+        return of(scmPathInfoStore.get().getRootUri().toASCIIString());
+      }
+    } catch (Exception e) {
+      log.debug("could not get ScmPathInfoStore from context", e);
+    }
+    return empty();
+  }
+
+  private String getPathFromConfiguration() {
+    log.debug("using base path from configuration: {}", scmConfiguration.getBaseUrl());
+    return scmConfiguration.getBaseUrl();
+  }
+
+  private class ProtocolWrapper extends HttpScmProtocol {
+
+    public ProtocolWrapper(Repository repository, String basePath) {
+      super(repository, basePath);
+    }
+
+    @Override
+    protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException {
+      if (!isInitialized) {
+        synchronized (InitializingHttpScmProtocolWrapper.this) {
+          if (!isInitialized) {
+            ScmProviderHttpServlet httpServlet = delegateProvider.get();
+            initializeServlet(config, httpServlet);
+            isInitialized = true;
+          }
+        }
+      }
+
+      delegateProvider.get().service(request, response, repository);
+    }
+
+  }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
index 2f436836cd..c66c56c0f1 100644
--- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java
@@ -33,8 +33,6 @@
 
 package sonia.scm.repository.spi;
 
-//~--- non-JDK imports --------------------------------------------------------
-
 import sonia.scm.repository.Feature;
 import sonia.scm.repository.api.Command;
 import sonia.scm.repository.api.CommandNotSupportedException;
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java
new file mode 100644
index 0000000000..3a9dad52d6
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java
@@ -0,0 +1,16 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.repository.Repository;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public interface ScmProviderHttpServlet {
+
+  void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException;
+
+  void init(ServletConfig config) throws ServletException;
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java
new file mode 100644
index 0000000000..c5dd55d277
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java
@@ -0,0 +1,28 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.repository.Repository;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class ScmProviderHttpServletDecorator implements ScmProviderHttpServlet {
+
+  private final ScmProviderHttpServlet object;
+
+  public ScmProviderHttpServletDecorator(ScmProviderHttpServlet object) {
+    this.object = object;
+  }
+
+  @Override
+  public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
+    object.service(request, response, repository);
+  }
+
+  @Override
+  public void init(ServletConfig config) throws ServletException {
+    object.init(config);
+  }
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java
new file mode 100644
index 0000000000..531a25e91d
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java
@@ -0,0 +1,15 @@
+package sonia.scm.repository.spi;
+
+import sonia.scm.DecoratorFactory;
+import sonia.scm.plugin.ExtensionPoint;
+
+@ExtensionPoint
+public interface ScmProviderHttpServletDecoratorFactory extends DecoratorFactory {
+  /**
+   * Has to return true if this factory provides a decorator for the given scm type (eg. "git", "hg" or
+   * "svn").
+   * @param type The current scm type this factory can provide a decorator for.
+   * @return true when the provided decorator should be used for the given scm type.
+   */
+  boolean handlesScmType(String type);
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java
new file mode 100644
index 0000000000..3793d4b935
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java
@@ -0,0 +1,33 @@
+package sonia.scm.repository.spi;
+
+import com.google.inject.Inject;
+import sonia.scm.util.Decorators;
+
+import javax.inject.Provider;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+
+public abstract class ScmProviderHttpServletProvider implements Provider {
+
+  @Inject(optional = true)
+  private Set decoratorFactories;
+
+  private final String type;
+
+  protected ScmProviderHttpServletProvider(String type) {
+    this.type = type;
+  }
+
+  @Override
+  public ScmProviderHttpServlet get() {
+    return Decorators.decorate(getRootServlet(), getDecoratorsForType());
+  }
+
+  private List getDecoratorsForType() {
+    return decoratorFactories.stream().filter(d -> d.handlesScmType(type)).collect(toList());
+  }
+
+  protected abstract ScmProviderHttpServlet getRootServlet();
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java b/scm-core/src/main/java/sonia/scm/util/Decorators.java
similarity index 99%
rename from scm-webapp/src/main/java/sonia/scm/util/Decorators.java
rename to scm-core/src/main/java/sonia/scm/util/Decorators.java
index 41c75660a9..6465631d03 100644
--- a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java
+++ b/scm-core/src/main/java/sonia/scm/util/Decorators.java
@@ -37,7 +37,6 @@ package sonia.scm.util;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-
 import sonia.scm.DecoratorFactory;
 
 /**
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
index dd3c82e800..328494a626 100644
--- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
+++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java
@@ -33,39 +33,32 @@
 
 package sonia.scm.web.filter;
 
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Splitter;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.authz.AuthorizationException;
 import org.apache.shiro.subject.Subject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import sonia.scm.ArgumentIsInvalidException;
 import sonia.scm.SCMContext;
 import sonia.scm.config.ScmConfiguration;
 import sonia.scm.repository.Repository;
 import sonia.scm.repository.RepositoryPermissions;
+import sonia.scm.repository.spi.ScmProviderHttpServlet;
+import sonia.scm.repository.spi.ScmProviderHttpServletDecorator;
 import sonia.scm.security.Role;
 import sonia.scm.security.ScmSecurityException;
 import sonia.scm.util.HttpUtil;
-import sonia.scm.util.Util;
 
-import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import java.util.Iterator;
-
-//~--- JDK imports ------------------------------------------------------------
 
 /**
  * Abstract http filter to check repository permissions.
  *
  * @author Sebastian Sdorra
  */
-public abstract class PermissionFilter extends HttpFilter
+public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
 {
 
   /** the logger for PermissionFilter */
@@ -81,23 +74,14 @@ public abstract class PermissionFilter extends HttpFilter
    *
    * @since 1.21
    */
-  public PermissionFilter(ScmConfiguration configuration)
+  protected PermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate)
   {
+    super(delegate);
     this.configuration = configuration;
   }
 
   //~--- get methods ----------------------------------------------------------
 
-  /**
-   * Returns the requested repository.
-   *
-   *
-   * @param request current http request
-   *
-   * @return requested repository
-   */
-  protected abstract Repository getRepository(HttpServletRequest request);
-
   /**
    * Returns true if the current request is a write request.
    *
@@ -117,66 +101,38 @@ public abstract class PermissionFilter extends HttpFilter
    *
    * @param request http request
    * @param response http response
-   * @param chain filter chain
    *
    * @throws IOException
    * @throws ServletException
    */
   @Override
-  protected void doFilter(HttpServletRequest request,
-    HttpServletResponse response, FilterChain chain)
+  public void service(HttpServletRequest request,
+    HttpServletResponse response, Repository repository)
     throws IOException, ServletException
   {
     Subject subject = SecurityUtils.getSubject();
 
     try
     {
-      Repository repository = getRepository(request);
+      boolean writeRequest = isWriteRequest(request);
 
-      if (repository != null)
+      if (hasPermission(repository, writeRequest))
       {
-        boolean writeRequest = isWriteRequest(request);
+        logger.trace("{} access to repository {} for user {} granted",
+          getActionAsString(writeRequest), repository.getName(),
+          getUserName(subject));
 
-        if (hasPermission(repository, writeRequest))
-        {
-          logger.trace("{} access to repository {} for user {} granted",
-            getActionAsString(writeRequest), repository.getName(),
-            getUserName(subject));
-
-          chain.doFilter(request, response);
-        }
-        else
-        {
-          logger.info("{} access to repository {} for user {} denied",
-            getActionAsString(writeRequest), repository.getName(),
-            getUserName(subject));
-          
-          sendAccessDenied(request, response, subject);
-        }
+        super.service(request, response, repository);
       }
       else
       {
-        logger.debug("repository not found");
+        logger.info("{} access to repository {} for user {} denied",
+          getActionAsString(writeRequest), repository.getName(),
+          getUserName(subject));
 
-        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        sendAccessDenied(request, response, subject);
       }
     }
-    catch (ArgumentIsInvalidException ex)
-    {
-      if (logger.isTraceEnabled())
-      {
-        logger.trace(
-          "wrong request at ".concat(request.getRequestURI()).concat(
-            " send redirect"), ex);
-      }
-      else if (logger.isWarnEnabled())
-      {
-        logger.warn("wrong request at {} send redirect",
-          request.getRequestURI());
-      }
-
-      response.sendRedirect(getRepositoryRootHelpUrl(request));
-    }
     catch (ScmSecurityException | AuthorizationException ex)
     {
       logger.warn("user " + subject.getPrincipal() +  " has not enough permissions", ex);
@@ -217,29 +173,6 @@ public abstract class PermissionFilter extends HttpFilter
     HttpUtil.sendUnauthorized(response, configuration.getRealmDescription());
   }
 
-  /**
-   * Extracts the type of the repositroy from url.
-   *
-   *
-   * @param request http request
-   *
-   * @return type of repository
-   */
-  private String extractType(HttpServletRequest request)
-  {
-    Iterator it = Splitter.on(
-                            HttpUtil.SEPARATOR_PATH).omitEmptyStrings().split(
-                            request.getRequestURI()).iterator();
-    String type = it.next();
-
-    if (Util.isNotEmpty(request.getContextPath()))
-    {
-      type = it.next();
-    }
-
-    return type;
-  }
-
   /**
    * Send access denied to the servlet response.
    *
@@ -280,25 +213,6 @@ public abstract class PermissionFilter extends HttpFilter
       : "read";
   }
 
-  /**
-   * Returns the repository root help url.
-   *
-   *
-   * @param request current http request
-   *
-   * @return repository root help url
-   */
-  private String getRepositoryRootHelpUrl(HttpServletRequest request)
-  {
-    String type = extractType(request);
-    String helpUrl = HttpUtil.getCompleteUrl(request,
-                       "/api/rest/help/repository-root/");
-
-    helpUrl = helpUrl.concat(type).concat(".html");
-
-    return helpUrl;
-  }
-
   /**
    * Returns the username from the given subject or anonymous.
    *
diff --git a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java
deleted file mode 100644
index ea0d90c915..0000000000
--- a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- *    this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- *    contributors may be used to endorse or promote products derived from this
- *    software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.web.filter;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Throwables;
-import com.google.inject.ProvisionException;
-
-import org.apache.shiro.authz.AuthorizationException;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import sonia.scm.config.ScmConfiguration;
-import sonia.scm.repository.Repository;
-import sonia.scm.repository.RepositoryProvider;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import javax.servlet.http.HttpServletRequest;
-
-/**
- *
- * @author Sebastian Sdorra
- * @since 1.9
- */
-public abstract class ProviderPermissionFilter extends PermissionFilter
-{
-
-  /**
-   * the logger for ProviderPermissionFilter
-   */
-  private static final Logger logger =
-    LoggerFactory.getLogger(ProviderPermissionFilter.class);
-
-  //~--- constructors ---------------------------------------------------------
-
-  /**
-   * Constructs ...
-   *
-   *
-   * @param configuration
-   * @param repositoryProvider
-   * @since 1.21
-   */
-  public ProviderPermissionFilter(ScmConfiguration configuration,
-    RepositoryProvider repositoryProvider)
-  {
-    super(configuration);
-    this.repositoryProvider = repositoryProvider;
-  }
-
-  //~--- get methods ----------------------------------------------------------
-
-  /**
-   * Method description
-   *
-   *
-   * @param request
-   *
-   * @return
-   */
-  @Override
-  protected Repository getRepository(HttpServletRequest request)
-  {
-    Repository repository = null;
-
-    try
-    {
-      repository = repositoryProvider.get();
-    }
-    catch (ProvisionException ex)
-    {
-      Throwables.propagateIfPossible(ex.getCause(),
-        IllegalStateException.class, AuthorizationException.class);
-      logger.error("could not get repository from request", ex);
-    }
-
-    return repository;
-  }
-
-  //~--- fields ---------------------------------------------------------------
-
-  /** Field description */
-  private final RepositoryProvider repositoryProvider;
-}
diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java
deleted file mode 100644
index f13f4cbc67..0000000000
--- a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- *    this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- *    contributors may be used to endorse or promote products derived from this
- *    software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-package sonia.scm.repository;
-
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-
-/**
- *
- * @author Sebastian Sdorra
- */
-public class RepositoryTest
-{
-
-  /**
-   * Method description
-   *
-   */
-  @Test
-  public void testCreateUrl()
-  {
-    Repository repository = new Repository("123", "hg", "test", "repo");
-
-    assertEquals("http://localhost:8080/scm/hg/test/repo",
-                 repository.createUrl("http://localhost:8080/scm"));
-    assertEquals("http://localhost:8080/scm/hg/test/repo",
-                 repository.createUrl("http://localhost:8080/scm/"));
-  }
-}
diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
new file mode 100644
index 0000000000..2ceafc19bb
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java
@@ -0,0 +1,74 @@
+package sonia.scm.repository.api;
+
+import org.junit.Test;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.spi.HttpScmProtocol;
+import sonia.scm.repository.spi.RepositoryServiceProvider;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.util.IterableUtil.sizeOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+public class RepositoryServiceTest {
+
+  private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class);
+  private final Repository repository = new Repository("", "git", "space", "repo");
+
+  @Test
+  public void shouldReturnMatchingProtocolsFromProvider() {
+    RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+    Stream supportedProtocols = repositoryService.getSupportedProtocols();
+
+    assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1);
+  }
+
+  @Test
+  public void shouldFindKnownProtocol() {
+    RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+
+    HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
+
+    assertThat(protocol).isNotNull();
+  }
+
+  @Test
+  public void shouldFailForUnknownProtocol() {
+    RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
+
+    assertThrows(IllegalArgumentException.class, () -> {
+      repositoryService.getProtocol(UnknownScmProtocol.class);
+    });
+  }
+
+  private static class DummyHttpProtocol extends HttpScmProtocol {
+    public DummyHttpProtocol(Repository repository) {
+      super(repository, "");
+    }
+
+    @Override
+    public void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) {
+    }
+  }
+
+  private static class DummyScmProtocolProvider implements ScmProtocolProvider {
+    @Override
+    public String getType() {
+      return "git";
+    }
+
+    @Override
+    public ScmProtocol get(Repository repository) {
+      return new DummyHttpProtocol(repository);
+    }
+  }
+
+  private interface UnknownScmProtocol extends ScmProtocol {}
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java
new file mode 100644
index 0000000000..1fd772fee3
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java
@@ -0,0 +1,40 @@
+package sonia.scm.repository.spi;
+
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import sonia.scm.repository.Repository;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HttpScmProtocolTest {
+
+  @TestFactory
+  Stream shouldCreateCorrectUrlsWithContextPath() {
+    return Stream.of("http://localhost/scm", "http://localhost/scm/")
+      .map(url -> assertResultingUrl(url, "http://localhost/scm/repo/space/name"));
+  }
+
+  @TestFactory
+  Stream shouldCreateCorrectUrlsWithPort() {
+    return Stream.of("http://localhost:8080", "http://localhost:8080/")
+      .map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name"));
+  }
+
+  DynamicTest assertResultingUrl(String baseUrl, String expectedUrl) {
+    String actualUrl = createInstanceOfHttpScmProtocol(baseUrl).getUrl();
+    return DynamicTest.dynamicTest(baseUrl + " -> " + expectedUrl, () -> assertThat(actualUrl).isEqualTo(expectedUrl));
+  }
+
+  private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) {
+    return new HttpScmProtocol(new Repository("", "", "space", "name"), baseUrl) {
+      @Override
+      protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) {
+      }
+    };
+  }
+}
diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java
new file mode 100644
index 0000000000..8c910f92a9
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java
@@ -0,0 +1,120 @@
+package sonia.scm.repository.spi;
+
+import com.google.inject.ProvisionException;
+import com.google.inject.util.Providers;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.stubbing.OngoingStubbing;
+import sonia.scm.api.v2.resources.ScmPathInfo;
+import sonia.scm.api.v2.resources.ScmPathInfoStore;
+import sonia.scm.config.ScmConfiguration;
+import sonia.scm.repository.Repository;
+
+import javax.inject.Provider;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+public class InitializingHttpScmProtocolWrapperTest {
+
+  private static final Repository REPOSITORY = new Repository("", "git", "space", "name");
+
+  @Mock
+  private ScmProviderHttpServlet delegateServlet;
+  @Mock
+  private ScmPathInfoStore pathInfoStore;
+  @Mock
+  private ScmConfiguration scmConfiguration;
+  private Provider pathInfoStoreProvider;
+
+  @Mock
+  private HttpServletRequest request;
+  @Mock
+  private HttpServletResponse response;
+  @Mock
+  private ServletConfig servletConfig;
+
+  private InitializingHttpScmProtocolWrapper wrapper;
+
+  @Before
+  public void init() {
+    initMocks(this);
+    pathInfoStoreProvider = mock(Provider.class);
+    when(pathInfoStoreProvider.get()).thenReturn(pathInfoStore);
+
+    wrapper = new InitializingHttpScmProtocolWrapper(Providers.of(this.delegateServlet), pathInfoStoreProvider, scmConfiguration) {
+      @Override
+      public String getType() {
+        return "git";
+      }
+    };
+    when(scmConfiguration.getBaseUrl()).thenReturn("http://example.com/scm");
+  }
+
+  @Test
+  public void shouldUsePathFromPathInfo() {
+    mockSetPathInfo();
+
+    HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+    assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
+  }
+
+  @Test
+  public void shouldUseConfigurationWhenPathInfoNotSet() {
+    HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+    assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
+  }
+
+  @Test
+  public void shouldUseConfigurationWhenNotInRequestScope() {
+    when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test"));
+
+    HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+    assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
+  }
+
+  @Test
+  public void shouldInitializeAndDelegateRequestThroughFilter() throws ServletException, IOException {
+    HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+    httpScmProtocol.serve(request, response, servletConfig);
+
+    verify(delegateServlet).init(servletConfig);
+    verify(delegateServlet).service(request, response, REPOSITORY);
+  }
+
+  @Test
+  public void shouldInitializeOnlyOnce() throws ServletException, IOException {
+    HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
+
+    httpScmProtocol.serve(request, response, servletConfig);
+    httpScmProtocol.serve(request, response, servletConfig);
+
+    verify(delegateServlet, times(1)).init(servletConfig);
+    verify(delegateServlet, times(2)).service(request, response, REPOSITORY);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void shouldFailForIllegalScmType() {
+    HttpScmProtocol httpScmProtocol = wrapper.get(new Repository("", "other", "space", "name"));
+  }
+
+  private OngoingStubbing mockSetPathInfo() {
+    return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/"));
+  }
+
+}
diff --git a/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java
new file mode 100644
index 0000000000..9fa65d51b8
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java
@@ -0,0 +1,74 @@
+package sonia.scm.web.filter;
+
+import com.github.sdorra.shiro.ShiroRule;
+import com.github.sdorra.shiro.SubjectAware;
+import org.junit.Rule;
+import org.junit.Test;
+import sonia.scm.config.ScmConfiguration;
+import sonia.scm.repository.Repository;
+import sonia.scm.repository.spi.ScmProviderHttpServlet;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+@SubjectAware(configuration = "classpath:sonia/scm/shiro.ini")
+public class PermissionFilterTest {
+
+  public static final Repository REPOSITORY = new Repository("1", "git", "space", "name");
+
+  @Rule
+  public final ShiroRule shiroRule = new ShiroRule();
+
+  private final ScmProviderHttpServlet delegateServlet = mock(ScmProviderHttpServlet.class);
+
+  private final PermissionFilter permissionFilter = new PermissionFilter(new ScmConfiguration(), delegateServlet) {
+    @Override
+    protected boolean isWriteRequest(HttpServletRequest request) {
+      return writeRequest;
+    }
+  };
+
+  private final HttpServletRequest request = mock(HttpServletRequest.class);
+  private final HttpServletResponse response = mock(HttpServletResponse.class);
+
+  private boolean writeRequest = false;
+
+  @Test
+  @SubjectAware(username = "reader", password = "secret")
+  public void shouldPassForReaderOnReadRequest() throws IOException, ServletException {
+    writeRequest = false;
+
+    permissionFilter.service(request, response, REPOSITORY);
+
+    verify(delegateServlet).service(request, response, REPOSITORY);
+  }
+
+  @Test
+  @SubjectAware(username = "reader", password = "secret")
+  public void shouldBlockForReaderOnWriteRequest() throws IOException, ServletException {
+    writeRequest = true;
+
+    permissionFilter.service(request, response, REPOSITORY);
+
+    verify(response).sendError(eq(401), anyString());
+    verify(delegateServlet, never()).service(request, response, REPOSITORY);
+  }
+
+  @Test
+  @SubjectAware(username = "writer", password = "secret")
+  public void shouldPassForWriterOnWriteRequest() throws IOException, ServletException {
+    writeRequest = true;
+
+    permissionFilter.service(request, response, REPOSITORY);
+
+    verify(delegateServlet).service(request, response, REPOSITORY);
+  }
+}
diff --git a/scm-core/src/test/resources/sonia/scm/shiro.ini b/scm-core/src/test/resources/sonia/scm/shiro.ini
index e87c81b097..fbdd35ba50 100644
--- a/scm-core/src/test/resources/sonia/scm/shiro.ini
+++ b/scm-core/src/test/resources/sonia/scm/shiro.ini
@@ -1,6 +1,12 @@
 [users]
 trillian = secret, user
+admin = secret, admin
+writer = secret, repo_write
+reader = secret, repo_read
+unpriv = secret
 
 [roles]
 admin = *
-user = something:*
\ No newline at end of file
+user = something:*
+repo_read = "repository:read:1"
+repo_write = "repository:push:1"
diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
index 2987780bcb..11db0200f1 100644
--- a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
+++ b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java
@@ -31,7 +31,7 @@ public class RepositoryUtil {
   public static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
     String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
       .extract()
-      .path("_links.httpProtocol.href");
+      .path("_links.protocol.find{it.name=='http'}.href");
 
     return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder);
   }
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java
index 7163497487..7607b31faf 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java
@@ -18,7 +18,7 @@ import static de.otto.edison.hal.Links.linkingTo;
 public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper {
 
   @Inject
-  private UriInfoStore uriInfoStore;
+  private ScmPathInfoStore scmPathInfoStore;
 
   @AfterMapping
   void appendLinks(GitConfig config, @MappingTarget GitConfigDto target) {
@@ -30,12 +30,12 @@ public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper webTokenGenerators)
-  {
-    super(configuration, webTokenGenerators);
-  }
-}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java
index 1f07753f1e..e38f26a309 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java
@@ -33,38 +33,24 @@
 
 package sonia.scm.web;
 
-//~--- non-JDK imports --------------------------------------------------------
-
 import com.google.common.annotations.VisibleForTesting;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
 import org.eclipse.jgit.http.server.GitSmartHttpTools;
-
 import sonia.scm.ClientMessages;
 import sonia.scm.config.ScmConfiguration;
 import sonia.scm.repository.GitUtil;
-import sonia.scm.repository.RepositoryProvider;
-import sonia.scm.web.filter.ProviderPermissionFilter;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import java.io.IOException;
+import sonia.scm.repository.spi.ScmProviderHttpServlet;
+import sonia.scm.web.filter.PermissionFilter;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import sonia.scm.Priority;
-import sonia.scm.filter.Filters;
-import sonia.scm.filter.WebElement;
+import java.io.IOException;
 
 /**
  * GitPermissionFilter decides if a git request requires write or read privileges.
  * 
  * @author Sebastian Sdorra
  */
-@Priority(Filters.PRIORITY_AUTHORIZATION)
-@WebElement(value = GitServletModule.PATTERN_GIT)
-public class GitPermissionFilter extends ProviderPermissionFilter
+public class GitPermissionFilter extends PermissionFilter
 {
 
   private static final String PARAMETER_SERVICE = "service";
@@ -83,11 +69,9 @@ public class GitPermissionFilter extends ProviderPermissionFilter
    * Constructs a new instance of the GitPermissionFilter.
    *
    * @param configuration scm main configuration
-   * @param repositoryProvider repository provider
    */
-  @Inject
-  public GitPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider) {
-    super(configuration, repositoryProvider);
+  public GitPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) {
+    super(configuration, delegate);
   }
 
   @Override
@@ -103,7 +87,7 @@ public class GitPermissionFilter extends ProviderPermissionFilter
   }
 
   @Override
-  protected boolean isWriteRequest(HttpServletRequest request) {
+  public boolean isWriteRequest(HttpServletRequest request) {
     return isReceivePackRequest(request) ||
         isReceiveServiceRequest(request) ||
         isLfsFileUpload(request);
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java
new file mode 100644
index 0000000000..c358da5fb1
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java
@@ -0,0 +1,30 @@
+package sonia.scm.web;
+
+import sonia.scm.config.ScmConfiguration;
+import sonia.scm.plugin.Extension;
+import sonia.scm.repository.GitRepositoryHandler;
+import sonia.scm.repository.spi.ScmProviderHttpServlet;
+import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory;
+
+import javax.inject.Inject;
+
+@Extension
+public class GitPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory {
+
+  private final ScmConfiguration configuration;
+
+  @Inject
+  public GitPermissionFilterFactory(ScmConfiguration configuration) {
+    this.configuration = configuration;
+  }
+
+  @Override
+  public boolean handlesScmType(String type) {
+    return GitRepositoryHandler.TYPE_NAME.equals(type);
+  }
+
+  @Override
+  public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) {
+    return new GitPermissionFilter(configuration, delegate);
+  }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java
index 76e742a71a..7f04bb3a54 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java
@@ -125,8 +125,9 @@ public class GitRepositoryResolver implements RepositoryResolver uriInfoStore, ScmConfiguration scmConfiguration) {
+    super(servletProvider, uriInfoStore, scmConfiguration);
+  }
+
+  @Override
+  public String getType() {
+    return GitRepositoryHandler.TYPE_NAME;
+  }
+}
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java
index bdad103c15..e731e01a62 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java
@@ -51,18 +51,6 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory;
 public class GitServletModule extends ServletModule
 {
 
-  public static final String GIT_PATH = "/git";
-
-  /** Field description */
-  public static final String PATTERN_GIT = GIT_PATH + "/*";
-
-
-  //~--- methods --------------------------------------------------------------
-
-  /**
-   * Method description
-   *
-   */
   @Override
   protected void configureServlets()
   {
@@ -75,8 +63,5 @@ public class GitServletModule extends ServletModule
 
     bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
     bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
-
-    // serlvelts and filters
-    serve(PATTERN_GIT).with(ScmGitServlet.class);
   }
 }
diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java
index 5612a64652..2701764607 100644
--- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java
+++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java
@@ -33,8 +33,6 @@
 
 package sonia.scm.web;
 
-//~--- non-JDK imports --------------------------------------------------------
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -42,8 +40,8 @@ import org.eclipse.jgit.http.server.GitServlet;
 import org.eclipse.jgit.lfs.lib.Constants;
 import org.slf4j.Logger;
 import sonia.scm.repository.Repository;
-import sonia.scm.repository.RepositoryProvider;
 import sonia.scm.repository.RepositoryRequestListenerUtil;
+import sonia.scm.repository.spi.ScmProviderHttpServlet;
 import sonia.scm.util.HttpUtil;
 import sonia.scm.web.lfs.servlet.LfsServletFactory;
 
@@ -57,19 +55,18 @@ import java.util.regex.Pattern;
 import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON;
 import static org.slf4j.LoggerFactory.getLogger;
 
-//~--- JDK imports ------------------------------------------------------------
-
 /**
  *
  * @author Sebastian Sdorra
  */
 @Singleton
-public class ScmGitServlet extends GitServlet
+public class ScmGitServlet extends GitServlet implements ScmProviderHttpServlet
 {
 
-  /** Field description */
+  public static final String REPO_PATH = "/repo";
+
   public static final Pattern REGEX_GITHTTPBACKEND = Pattern.compile(
-    "(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$"
+    "(?x)^/repo/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$"
   );
 
   /** Field description */
@@ -88,7 +85,6 @@ public class ScmGitServlet extends GitServlet
    * @param repositoryResolver
    * @param receivePackFactory
    * @param repositoryViewer
-   * @param repositoryProvider
    * @param repositoryRequestListenerUtil
    * @param lfsServletFactory
    */
@@ -96,11 +92,9 @@ public class ScmGitServlet extends GitServlet
   public ScmGitServlet(GitRepositoryResolver repositoryResolver,
                        GitReceivePackFactory receivePackFactory,
                        GitRepositoryViewer repositoryViewer,
-                       RepositoryProvider repositoryProvider,
                        RepositoryRequestListenerUtil repositoryRequestListenerUtil,
                        LfsServletFactory lfsServletFactory)
   {
-    this.repositoryProvider = repositoryProvider;
     this.repositoryViewer = repositoryViewer;
     this.repositoryRequestListenerUtil = repositoryRequestListenerUtil;
     this.lfsServletFactory = lfsServletFactory;
@@ -122,44 +116,9 @@ public class ScmGitServlet extends GitServlet
    * @throws ServletException
    */
   @Override
-  protected void service(HttpServletRequest request,
-    HttpServletResponse response)
+  public void service(HttpServletRequest request, HttpServletResponse response, Repository repository)
     throws ServletException, IOException
   {    
-    Repository repository = repositoryProvider.get();
-    if (repository != null) {
-      handleRequest(request, response, repository);
-    } else {
-      // logger
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-    }
-  }
-  
-  /**
-   * Decides the type request being currently made and delegates it accordingly.
-   * 
    - *
  • Batch API:
  • - *
      - *
    • used to provide the client with information on how handle the large files of a repository.
    • - *
    • response contains the information where to perform the actual upload and download of the large objects.
    • - *
    - *
  • Transfer API:
  • - *
      - *
    • receives and provides the actual large objects (resolves the pointer placed in the file of the working copy).
    • - *
    • invoked only after the Batch API has been questioned about what to do with the large files
    • - *
    - *
  • Regular Git Http API:
  • - *
      - *
    • regular git http wire protocol, use by normal git clients.
    • - *
    - *
  • Browser Overview:
  • - *
      - *
    • short repository overview for browser clients.
    • - *
    - *
  • - *
- */ - private void handleRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { String repoPath = repository.getNamespace() + "/" + repository.getName(); logger.trace("handle git repository at {}", repoPath); if (isLfsBatchApiRequest(request, repoPath)) { @@ -210,7 +169,7 @@ public class ScmGitServlet extends GitServlet * @throws IOException * @throws ServletException */ - private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException { try { repositoryViewer.handleRequest(request, response, repository); } catch (IOException ex) { @@ -229,7 +188,7 @@ public class ScmGitServlet extends GitServlet */ private static boolean isLfsFileTransferRequest(HttpServletRequest request, String repository) { - String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), GitServletModule.GIT_PATH, repository); + String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), REPO_PATH, repository); boolean pathMatches = request.getRequestURI().matches(regex); boolean methodMatches = request.getMethod().equals("PUT") || request.getMethod().equals("GET"); @@ -248,7 +207,7 @@ public class ScmGitServlet extends GitServlet */ private static boolean isLfsBatchApiRequest(HttpServletRequest request, String repository) { - String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), GitServletModule.GIT_PATH, repository); + String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), REPO_PATH, repository); boolean pathMatches = request.getRequestURI().matches(regex); boolean methodMatches = "POST".equals(request.getMethod()); @@ -284,12 +243,8 @@ public class ScmGitServlet extends GitServlet return false; } - //~--- fields --------------------------------------------------------------- - /** Field description */ - private final RepositoryProvider repositoryProvider; - /** Field description */ private final RepositoryRequestListenerUtil repositoryRequestListenerUtil; @@ -299,5 +254,4 @@ public class ScmGitServlet extends GitServlet private final GitRepositoryViewer repositoryViewer; private final LfsServletFactory lfsServletFactory; - } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java new file mode 100644 index 0000000000..56a9e358be --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class ScmGitServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider servletProvider; + + public ScmGitServletProvider() { + super(GitRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java index 58bdb2fcf1..f4eed34678 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java @@ -70,7 +70,7 @@ public class LfsServletFactory { */ @VisibleForTesting static String buildBaseUri(Repository repository, HttpServletRequest request) { - return String.format("%s/git/%s/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getNamespace(), repository.getName()); + return String.format("%s/repo/%s/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getNamespace(), repository.getName()); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java index 42790ea7a4..1ebe7fc98b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java @@ -53,7 +53,7 @@ public class GitConfigResourceTest { private GitConfigDtoToGitConfigMapperImpl dtoToConfigMapper; @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private GitConfigToGitConfigDtoMapperImpl configToDtoMapper; @@ -67,7 +67,7 @@ public class GitConfigResourceTest { when(repositoryHandler.getConfig()).thenReturn(gitConfig); GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); dispatcher.getRegistry().addSingletonResource(gitConfigResource); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java index 51fded4839..82c85029a3 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java @@ -28,7 +28,7 @@ public class GitConfigToGitConfigDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private GitConfigToGitConfigDtoMapperImpl mapper; @@ -40,7 +40,7 @@ public class GitConfigToGitConfigDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(GitConfigResource.GIT_CONFIG_PATH_V2); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java index fede8842a9..831402261b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java @@ -5,7 +5,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; -import sonia.scm.repository.RepositoryProvider; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.HttpUtil; import javax.servlet.ServletOutputStream; @@ -29,12 +29,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class GitPermissionFilterTest { - @Mock - private RepositoryProvider repositoryProvider; - - private final GitPermissionFilter permissionFilter = new GitPermissionFilter( - new ScmConfiguration(), repositoryProvider - ); + private final GitPermissionFilter permissionFilter = new GitPermissionFilter(new ScmConfiguration(), mock(ScmProviderHttpServlet.class)); @Mock private HttpServletResponse response; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java index f6f5803bb7..09a431ea43 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java @@ -23,12 +23,12 @@ public class LfsServletFactoryTest { String repositoryName = "git-lfs-demo"; String result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, true)); - assertThat(result, is(equalTo("http://localhost:8081/scm/git/space/git-lfs-demo.git/info/lfs/objects/"))); + assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/"))); //result will be with dot-git suffix, ide result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, false)); - assertThat(result, is(equalTo("http://localhost:8081/scm/git/space/git-lfs-demo.git/info/lfs/objects/"))); + assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/"))); } private HttpServletRequest RequestWithUri(String repositoryName, boolean withDotGitSuffix) { @@ -44,12 +44,10 @@ public class LfsServletFactoryTest { //build from valid live request data when(mockedRequest.getRequestURL()).thenReturn( - new StringBuffer(String.format("http://localhost:8081/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix))); - when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix)); + new StringBuffer(String.format("http://localhost:8081/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix))); + when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix)); when(mockedRequest.getContextPath()).thenReturn("/scm"); return mockedRequest; } - - } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java index d2f4aecf7e..da980f75c0 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java @@ -8,11 +8,11 @@ import static de.otto.edison.hal.Links.linkingTo; public class HgConfigInstallationsToDtoMapper { - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @Inject - public HgConfigInstallationsToDtoMapper(UriInfoStore uriInfoStore) { - this.uriInfoStore = uriInfoStore; + public HgConfigInstallationsToDtoMapper(ScmPathInfoStore scmPathInfoStore) { + this.scmPathInfoStore = scmPathInfoStore; } public HgConfigInstallationsDto map(List installations, String path) { @@ -20,7 +20,7 @@ public class HgConfigInstallationsToDtoMapper { } private String createSelfLink(String path) { - LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); return linkBuilder.method("getInstallationsResource").parameters().href() + '/' + path; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java index 67d7e58dff..3ee87cef84 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java @@ -20,7 +20,7 @@ import static de.otto.edison.hal.Links.linkingTo; public abstract class HgConfigPackagesToDtoMapper { @Inject - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; public HgConfigPackagesDto map(HgPackages hgpackages) { return map(new HgPackagesNonIterable(hgpackages)); @@ -40,7 +40,7 @@ public abstract class HgConfigPackagesToDtoMapper { } private String createSelfLink() { - LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); return linkBuilder.method("getPackagesResource").parameters().href(); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java index 98137aebd5..b2a67e2aa4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java @@ -18,7 +18,7 @@ import static de.otto.edison.hal.Links.linkingTo; public abstract class HgConfigToHgConfigDtoMapper extends BaseMapper { @Inject - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @AfterMapping void appendLinks(HgConfig config, @MappingTarget HgConfigDto target) { @@ -30,12 +30,12 @@ public abstract class HgConfigToHgConfigDtoMapper extends BaseMapper webTokenGenerators) - { - super(configuration, webTokenGenerators); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - */ - @Override - protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { - if (HgUtil.isHgClient(request) - && (configuration.isLoginAttemptLimitEnabled())) - { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED); - } - else - { - super.sendFailedAuthenticationError(request, response); - } - } -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java index 6cb4d523f0..1821f92fa4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java @@ -33,8 +33,6 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.inject.Inject; @@ -49,8 +47,8 @@ import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgPythonScript; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRequestListenerUtil; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.security.CipherUtil; import sonia.scm.util.AssertUtil; import sonia.scm.util.HttpUtil; @@ -68,14 +66,12 @@ import java.io.IOException; import java.util.Base64; import java.util.Enumeration; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra */ @Singleton -public class HgCGIServlet extends HttpServlet +public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet { /** Field description */ @@ -108,20 +104,18 @@ public class HgCGIServlet extends HttpServlet * * @param cgiExecutorFactory * @param configuration - * @param repositoryProvider * @param handler * @param hookManager * @param requestListenerUtil */ @Inject public HgCGIServlet(CGIExecutorFactory cgiExecutorFactory, - ScmConfiguration configuration, RepositoryProvider repositoryProvider, + ScmConfiguration configuration, HgRepositoryHandler handler, HgHookManager hookManager, RepositoryRequestListenerUtil requestListenerUtil) { this.cgiExecutorFactory = cgiExecutorFactory; this.configuration = configuration; - this.repositoryProvider = repositoryProvider; this.handler = handler; this.hookManager = hookManager; this.requestListenerUtil = requestListenerUtil; @@ -131,46 +125,11 @@ public class HgCGIServlet extends HttpServlet //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @throws ServletException - */ @Override - public void init() throws ServletException + public void service(HttpServletRequest request, + HttpServletResponse response, Repository repository) { - - super.init(); - } - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void service(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - Repository repository = repositoryProvider.get(); - - if (repository == null) - { - if (logger.isDebugEnabled()) - { - logger.debug("no hg repository found at {}", request.getRequestURI()); - } - - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } - else if (!handler.isConfigured()) + if (!handler.isConfigured()) { exceptionHandler.sendFormattedError(request, response, HgCGIExceptionHandler.ERROR_NOT_CONFIGURED); @@ -379,9 +338,6 @@ public class HgCGIServlet extends HttpServlet /** Field description */ private final HgHookManager hookManager; - /** Field description */ - private final RepositoryProvider repositoryProvider; - /** Field description */ private final RepositoryRequestListenerUtil requestListenerUtil; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java new file mode 100644 index 0000000000..db7a6be7b3 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class HgCGIServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider servletProvider; + + public HgCGIServletProvider() { + super(HgRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index de2835dd8f..7f92cc0357 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -33,53 +33,31 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; -import com.google.inject.Inject; - -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.repository.RepositoryProvider; -import sonia.scm.web.filter.ProviderPermissionFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Set; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; import javax.servlet.http.HttpServletRequest; +import java.util.Set; /** * Permission filter for mercurial repositories. * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = HgServletModule.MAPPING_HG) -public class HgPermissionFilter extends ProviderPermissionFilter +public class HgPermissionFilter extends PermissionFilter { private static final Set READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE"); - /** - * Constructs a new instance. - * - * @param configuration scm configuration - * @param repositoryProvider repository provider - */ - @Inject - public HgPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repositoryProvider) + public HgPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { - super(configuration, repositoryProvider); + super(configuration, delegate); } - //~--- get methods ---------------------------------------------------------- - @Override - protected boolean isWriteRequest(HttpServletRequest request) + public boolean isWriteRequest(HttpServletRequest request) { return !READ_METHODS.contains(request.getMethod()); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java new file mode 100644 index 0000000000..90f53a1fea --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java @@ -0,0 +1,30 @@ +package sonia.scm.web; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +import javax.inject.Inject; + +@Extension +public class HgPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final ScmConfiguration configuration; + + @Inject + public HgPermissionFilterFactory(ScmConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public boolean handlesScmType(String type) { + return HgRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new HgPermissionFilter(configuration, delegate); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java new file mode 100644 index 0000000000..37360a5845 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java @@ -0,0 +1,25 @@ +package sonia.scm.web; + +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +@Singleton +@Extension +public class HgScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { + @Inject + public HgScmProtocolProviderWrapper(HgCGIServletProvider servletProvider, Provider uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + public String getType() { + return HgRepositoryHandler.TYPE_NAME; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java index 357995483d..ba9ae3a0b9 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java @@ -81,8 +81,5 @@ public class HgServletModule extends ServletModule // bind servlets serve(MAPPING_HOOK).with(HgHookCallbackServlet.class); - - // register hg cgi servlet - serve(MAPPING_HG).with(HgCGIServlet.class); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java index 540a5b6757..65b9c262cb 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java @@ -43,7 +43,7 @@ public class HgConfigInstallationsResourceTest { private final URI baseUri = URI.create("/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigInstallationsToDtoMapper mapper; @@ -61,7 +61,7 @@ public class HgConfigInstallationsResourceTest { new HgConfigResource(null, null, null, null, null, resourceProvider)); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java index 34048f80b2..7cae1d9f7e 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java @@ -23,7 +23,7 @@ public class HgConfigInstallationsToDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigInstallationsToDtoMapper mapper; @@ -34,7 +34,7 @@ public class HgConfigInstallationsToDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + expectedPath); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java index 044897ad80..f1558b6efb 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java @@ -57,7 +57,7 @@ public class HgConfigPackageResourceTest { private final URI baseUri = java.net.URI.create("/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigPackagesToDtoMapperImpl mapper; @@ -81,7 +81,7 @@ public class HgConfigPackageResourceTest { public void prepareEnvironment() { setupResources(); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); when(hgPackageReader.getPackages().getPackages()).thenReturn(createPackages()); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java index 671d9fb7e1..c4431da6d5 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import de.otto.edison.hal.HalRepresentation; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -13,12 +12,10 @@ import sonia.scm.installer.HgPackages; import java.net.URI; import java.util.Arrays; -import java.util.Collection; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import static sonia.scm.api.v2.resources.HgConfigTests.assertEqualsPackage; import static sonia.scm.api.v2.resources.HgConfigTests.createPackage; @@ -29,7 +26,7 @@ public class HgConfigPackagesToDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigPackagesToDtoMapperImpl mapper; @@ -38,7 +35,7 @@ public class HgConfigPackagesToDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/packages"); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java index 11a0fb55f9..9cd04a0789 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java @@ -54,7 +54,7 @@ public class HgConfigResourceTest { private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper; @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigToHgConfigDtoMapperImpl configToDtoMapper; @@ -79,7 +79,7 @@ public class HgConfigResourceTest { new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource, autoconfigResource, installationsResource); dispatcher.getRegistry().addSingletonResource(gitConfigResource); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java index a12e95926c..81c50f3d58 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java @@ -29,7 +29,7 @@ public class HgConfigToHgConfigDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigToHgConfigDtoMapperImpl mapper; @@ -41,7 +41,7 @@ public class HgConfigToHgConfigDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java index a71d75151d..c160280822 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java @@ -18,7 +18,7 @@ import static de.otto.edison.hal.Links.linkingTo; public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper { @Inject - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @AfterMapping void appendLinks(SvnConfig config, @MappingTarget SvnConfigDto target) { @@ -30,12 +30,12 @@ public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper webTokenGenerators) { - super(configuration, webTokenGenerators); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Sends unauthorized instead of forbidden for svn clients, because the - * svn client prompts again for authentication. - * - * - * @param request http request - * @param response http response - * - * @throws IOException - */ - @Override - protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { - if (SvnUtil.isSvnClient(request)) - { - HttpUtil.sendUnauthorized(response, configuration.getRealmDescription()); - } - else - { - super.sendFailedAuthenticationError(request, response); - } - } -} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java index c811179255..92d01db5a1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java @@ -33,8 +33,6 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; import com.google.inject.Singleton; import org.slf4j.Logger; @@ -45,6 +43,7 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRequestListenerUtil; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.AssertUtil; import sonia.scm.util.HttpUtil; @@ -54,14 +53,12 @@ import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra */ @Singleton -public class SvnDAVServlet extends DAVServlet +public class SvnDAVServlet extends DAVServlet implements ScmProviderHttpServlet { /** Field description */ @@ -110,28 +107,18 @@ public class SvnDAVServlet extends DAVServlet * @throws ServletException */ @Override - public void service(HttpServletRequest request, HttpServletResponse response) + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { - Repository repository = repositoryProvider.get(); - - if (repository != null) - { - if (repositoryRequestListenerUtil.callListeners(request, response, - repository)) - { - super.service(new SvnHttpServletRequestWrapper(request, - repositoryProvider), response); - } - else if (logger.isDebugEnabled()) - { - logger.debug("request aborted by repository request listener"); - } - } - else + if (repositoryRequestListenerUtil.callListeners(request, response, + repository)) { super.service(new SvnHttpServletRequestWrapper(request, - repositoryProvider), response); + repository), response); + } + else if (logger.isDebugEnabled()) + { + logger.debug("request aborted by repository request listener"); } } @@ -163,18 +150,11 @@ public class SvnDAVServlet extends DAVServlet extends HttpServletRequestWrapper { - /** - * Constructs ... - * - * - * @param request - * @param repositoryProvider - */ public SvnHttpServletRequestWrapper(HttpServletRequest request, - RepositoryProvider repositoryProvider) + Repository repository) { super(request); - this.repositoryProvider = repositoryProvider; + this.repository = repository; } //~--- get methods -------------------------------------------------------- @@ -211,8 +191,6 @@ public class SvnDAVServlet extends DAVServlet AssertUtil.assertIsNotEmpty(pathInfo); - Repository repository = repositoryProvider.get(); - if (repository != null) { if (pathInfo.startsWith(HttpUtil.SEPARATOR_PATH)) @@ -236,7 +214,6 @@ public class SvnDAVServlet extends DAVServlet public String getServletPath() { String servletPath = super.getServletPath(); - Repository repository = repositoryProvider.get(); if (repository != null) { @@ -280,10 +257,9 @@ public class SvnDAVServlet extends DAVServlet //~--- fields ------------------------------------------------------------- /** Field description */ - private final RepositoryProvider repositoryProvider; + private final Repository repository; } - //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java new file mode 100644 index 0000000000..d221504256 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class SvnDAVServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider servletProvider; + + public SvnDAVServletProvider() { + super(SvnRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java index 65afefbb28..7cc78180ff 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java @@ -34,38 +34,34 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.Inject; -import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.filter.GZipFilter; +import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +//~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra */ -@Singleton -public class SvnGZipFilter extends GZipFilter +public class SvnGZipFilter extends GZipFilter implements ScmProviderHttpServlet { - /** - * the logger for SvnGZipFilter - */ - private static final Logger logger = - LoggerFactory.getLogger(SvnGZipFilter.class); + private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class); + + private final SvnRepositoryHandler handler; + private final ScmProviderHttpServlet delegate; //~--- constructors --------------------------------------------------------- @@ -75,10 +71,10 @@ public class SvnGZipFilter extends GZipFilter * * @param handler */ - @Inject - public SvnGZipFilter(SvnRepositoryHandler handler) + public SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) { this.handler = handler; + this.delegate = delegate; } //~--- methods -------------------------------------------------------------- @@ -134,8 +130,30 @@ public class SvnGZipFilter extends GZipFilter } } - //~--- fields --------------------------------------------------------------- + @Override + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + if (handler.getConfig().isEnabledGZip()) + { + if (logger.isTraceEnabled()) + { + logger.trace("encode svn request with gzip"); + } - /** Field description */ - private SvnRepositoryHandler handler; + super.doFilter(request, response, (servletRequest, servletResponse) -> delegate.service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, repository)); + } + else + { + if (logger.isTraceEnabled()) + { + logger.trace("skip gzip encoding"); + } + + delegate.service(request, response, repository); + } + } + + @Override + public void init(ServletConfig config) throws ServletException { + delegate.init(config); + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java new file mode 100644 index 0000000000..e2774106fa --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java @@ -0,0 +1,28 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +@Extension +public class SvnGZipFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final SvnRepositoryHandler handler; + + @Inject + public SvnGZipFilterFactory(SvnRepositoryHandler handler) { + this.handler = handler; + } + + @Override + public boolean handlesScmType(String type) { + return SvnRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new SvnGZipFilter(handler, delegate); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java index 30ef3e94c0..2c0a1e65ff 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java @@ -33,37 +33,24 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; -import com.google.inject.Inject; - import sonia.scm.ClientMessages; -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.ScmSvnErrorCode; import sonia.scm.repository.SvnUtil; -import sonia.scm.web.filter.ProviderPermissionFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; /** * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = SvnServletModule.PATTERN_SVN) -public class SvnPermissionFilter extends ProviderPermissionFilter +public class SvnPermissionFilter extends PermissionFilter { /** Field description */ @@ -77,13 +64,10 @@ public class SvnPermissionFilter extends ProviderPermissionFilter * Constructs ... * * @param configuration - * @param repository */ - @Inject - public SvnPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repository) + public SvnPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { - super(configuration, repository); + super(configuration, delegate); } //~--- methods -------------------------------------------------------------- @@ -132,7 +116,7 @@ public class SvnPermissionFilter extends ProviderPermissionFilter * @return */ @Override - protected boolean isWriteRequest(HttpServletRequest request) + public boolean isWriteRequest(HttpServletRequest request) { return WRITEMETHOD_SET.contains(request.getMethod().toUpperCase()); } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java new file mode 100644 index 0000000000..882cb8c54f --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java @@ -0,0 +1,30 @@ +package sonia.scm.web; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +import javax.inject.Inject; + +@Extension +public class SvnPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final ScmConfiguration configuration; + + @Inject + public SvnPermissionFilterFactory(ScmConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public boolean handlesScmType(String type) { + return SvnRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new SvnPermissionFilter(configuration, delegate); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java new file mode 100644 index 0000000000..ba7d5e875a --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java @@ -0,0 +1,74 @@ +package sonia.scm.web; + +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; +import sonia.scm.repository.spi.ScmProviderHttpServlet; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import java.util.Enumeration; + +@Singleton +@Extension +public class SvnScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { + + public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; + + @Override + public String getType() { + return SvnRepositoryHandler.TYPE_NAME; + } + + @Inject + public SvnScmProtocolProviderWrapper(SvnDAVServletProvider servletProvider, Provider uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException { + + super.initializeServlet(new SvnConfigEnhancer(config), httpServlet); + } + + private static class SvnConfigEnhancer implements ServletConfig { + + private final ServletConfig originalConfig; + + private SvnConfigEnhancer(ServletConfig originalConfig) { + this.originalConfig = originalConfig; + } + + @Override + public String getServletName() { + return originalConfig.getServletName(); + } + + @Override + public ServletContext getServletContext() { + return originalConfig.getServletContext(); + } + + @Override + /** + * Overridden to return the systems temp directory for the key {@link PARAMETER_SVN_PARENTPATH}. + */ + public String getInitParameter(String key) { + if (PARAMETER_SVN_PARENTPATH.equals(key)) { + return System.getProperty("java.io.tmpdir"); + } + return originalConfig.getInitParameter(key); + } + + @Override + public Enumeration getInitParameterNames() { + return originalConfig.getInitParameterNames(); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java index 9b5c8ae556..8526d6380a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java @@ -33,53 +33,22 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.servlet.ServletModule; import org.mapstruct.factory.Mappers; import sonia.scm.api.v2.resources.SvnConfigDtoToSvnConfigMapper; import sonia.scm.api.v2.resources.SvnConfigToSvnConfigDtoMapper; import sonia.scm.plugin.Extension; -import java.util.HashMap; -import java.util.Map; - -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra */ @Extension -public class SvnServletModule extends ServletModule -{ +public class SvnServletModule extends ServletModule { - /** Field description */ - public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; - - /** Field description */ - public static final String PATTERN_SVN = "/svn/*"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ @Override - protected void configureServlets() - { - filter(PATTERN_SVN).through(SvnGZipFilter.class); - filter(PATTERN_SVN).through(SvnBasicAuthenticationFilter.class); - filter(PATTERN_SVN).through(SvnPermissionFilter.class); - + protected void configureServlets() { bind(SvnConfigDtoToSvnConfigMapper.class).to(Mappers.getMapper(SvnConfigDtoToSvnConfigMapper.class).getClass()); bind(SvnConfigToSvnConfigDtoMapper.class).to(Mappers.getMapper(SvnConfigToSvnConfigDtoMapper.class).getClass()); - - Map parameters = new HashMap(); - - parameters.put(PARAMETER_SVN_PARENTPATH, - System.getProperty("java.io.tmpdir")); - serve(PATTERN_SVN).with(SvnDAVServlet.class, parameters); } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java index de4d654910..28dbe09e92 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java @@ -53,7 +53,7 @@ public class SvnConfigResourceTest { private SvnConfigDtoToSvnConfigMapperImpl dtoToConfigMapper; @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private SvnConfigToSvnConfigDtoMapperImpl configToDtoMapper; @@ -67,7 +67,7 @@ public class SvnConfigResourceTest { when(repositoryHandler.getConfig()).thenReturn(gitConfig); SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); dispatcher.getRegistry().addSingletonResource(gitConfigResource); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java index de7b0ecd31..5184aa3d41 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java @@ -30,7 +30,7 @@ public class SvnConfigToSvnConfigDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private SvnConfigToSvnConfigDtoMapperImpl mapper; @@ -42,7 +42,7 @@ public class SvnConfigToSvnConfigDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(SvnConfigResource.SVN_CONFIG_PATH_V2); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index da01814152..644a1bc0f9 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -49,7 +49,7 @@ scm-core 2.0.0-SNAPSHOT - + sonia.scm scm-dao-xml diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 1eaef333a1..5a087a1c70 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -33,8 +33,6 @@ package sonia.scm; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.inject.Injector; @@ -63,8 +61,6 @@ import java.util.Collections; import java.util.List; import java.util.Set; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra @@ -135,7 +131,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList moduleList.add(new EagerSingletonModule()); moduleList.add(ShiroWebModule.guiceFilterModule()); moduleList.add(new WebElementModule(pluginLoader)); - moduleList.add(new ScmServletModule(context, pluginLoader, overrides, pluginLoader.getExtensionProcessor())); + moduleList.add(new ScmServletModule(context, pluginLoader, overrides)); moduleList.add( new ScmSecurityModule(context, pluginLoader.getExtensionProcessor()) ); diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 1d627de8ed..e9ec9e4a39 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -33,8 +33,6 @@ package sonia.scm; -//~--- non-JDK imports -------------------------------------------------------- - import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Provider; import com.google.inject.multibindings.Multibinder; @@ -56,17 +54,48 @@ import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; import sonia.scm.net.SSLContextProvider; -import sonia.scm.net.ahc.*; -import sonia.scm.plugin.*; -import sonia.scm.repository.*; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.net.ahc.ContentTransformer; +import sonia.scm.net.ahc.DefaultAdvancedHttpClient; +import sonia.scm.net.ahc.JsonContentTransformer; +import sonia.scm.net.ahc.XmlContentTransformer; +import sonia.scm.plugin.DefaultPluginLoader; +import sonia.scm.plugin.DefaultPluginManager; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.PluginManager; +import sonia.scm.repository.DefaultRepositoryManager; +import sonia.scm.repository.DefaultRepositoryProvider; +import sonia.scm.repository.HealthCheckContextListener; +import sonia.scm.repository.NamespaceStrategy; +import sonia.scm.repository.NamespaceStrategyProvider; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryManagerProvider; +import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.schedule.QuartzScheduler; import sonia.scm.schedule.Scheduler; -import sonia.scm.security.*; -import sonia.scm.store.*; +import sonia.scm.security.AuthorizationChangedEventProducer; +import sonia.scm.security.CipherHandler; +import sonia.scm.security.CipherUtil; +import sonia.scm.security.ConfigurableLoginAttemptHandler; +import sonia.scm.security.DefaultKeyGenerator; +import sonia.scm.security.DefaultSecuritySystem; +import sonia.scm.security.KeyGenerator; +import sonia.scm.security.LoginAttemptHandler; +import sonia.scm.security.SecuritySystem; +import sonia.scm.store.BlobStoreFactory; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.store.DataStoreFactory; +import sonia.scm.store.FileBlobStoreFactory; +import sonia.scm.store.JAXBConfigurationEntryStoreFactory; +import sonia.scm.store.JAXBConfigurationStoreFactory; +import sonia.scm.store.JAXBDataStoreFactory; import sonia.scm.template.MustacheTemplateEngine; import sonia.scm.template.TemplateEngine; import sonia.scm.template.TemplateEngineFactory; @@ -81,14 +110,16 @@ import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.UserAgentParser; import sonia.scm.web.cgi.CGIExecutorFactory; import sonia.scm.web.cgi.DefaultCGIExecutorFactory; +import sonia.scm.web.filter.AuthenticationFilter; import sonia.scm.web.filter.LoggingFilter; +import sonia.scm.web.protocol.HttpProtocolServlet; import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.DefaultAdministrationContext; import javax.net.ssl.SSLContext; import javax.servlet.ServletContext; -//~--- JDK imports ------------------------------------------------------------ +import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; /** * @@ -99,14 +130,14 @@ public class ScmServletModule extends ServletModule /** Field description */ public static final String[] PATTERN_ADMIN = new String[] { - "/api/rest/groups*", - "/api/rest/users*", "/api/rest/plguins*" }; + REST_API_PATH + "/groups*", + REST_API_PATH + "/users*", REST_API_PATH + "/plguins*" }; /** Field description */ public static final String PATTERN_ALL = "/*"; /** Field description */ - public static final String PATTERN_CONFIG = "/api/rest/config*"; + public static final String PATTERN_CONFIG = REST_API_PATH + "/config*"; /** Field description */ public static final String PATTERN_DEBUG = "/debug.html"; @@ -155,22 +186,11 @@ public class ScmServletModule extends ServletModule //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param servletContext - * @param pluginLoader - * @param overrides - * @param extensionProcessor - */ - ScmServletModule(ServletContext servletContext, - DefaultPluginLoader pluginLoader, ClassOverrides overrides, ExtensionProcessor extensionProcessor) + ScmServletModule(ServletContext servletContext, DefaultPluginLoader pluginLoader, ClassOverrides overrides) { this.servletContext = servletContext; this.pluginLoader = pluginLoader; this.overrides = overrides; - this.extensionProcessor = extensionProcessor; } //~--- methods -------------------------------------------------------------- @@ -293,6 +313,8 @@ public class ScmServletModule extends ServletModule bind(TemplateEngineFactory.class); bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class); + filter(HttpProtocolServlet.PATTERN).through(AuthenticationFilter.class); + // bind events // bind(LastModifiedUpdateListener.class); @@ -389,11 +411,6 @@ public class ScmServletModule extends ServletModule /** * Load ScmConfiguration with JAXB - * - * - * @param context - * - * @return */ private ScmConfiguration getScmConfiguration() { @@ -414,6 +431,4 @@ public class ScmServletModule extends ServletModule /** Field description */ private final ServletContext servletContext; - - private final ExtensionProcessor extensionProcessor; } diff --git a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java index 98117851db..059c990718 100644 --- a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java @@ -33,7 +33,7 @@ public class WebResourceServlet extends HttpServlet { * TODO remove old protocol servlets and hook. Move /hook/hg to api? */ @VisibleForTesting - static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/).*"; + static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/|repo/).*"; private static final Logger LOG = LoggerFactory.getLogger(WebResourceServlet.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java b/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java index de0f1dd626..b602e918ea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java @@ -1,24 +1,26 @@ package sonia.scm.api.rest; -import sonia.scm.api.v2.resources.UriInfoStore; +import sonia.scm.api.v2.resources.ScmPathInfoStore; import javax.inject.Inject; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Provider; @Provider public class UriInfoFilter implements ContainerRequestFilter { - private final javax.inject.Provider storeProvider; + private final javax.inject.Provider storeProvider; @Inject - public UriInfoFilter(javax.inject.Provider storeProvider) { + public UriInfoFilter(javax.inject.Provider storeProvider) { this.storeProvider = storeProvider; } @Override public void filter(ContainerRequestContext requestContext) { - storeProvider.get().set(requestContext.getUriInfo()); + UriInfo uriInfo = requestContext.getUriInfo(); + storeProvider.get().set(uriInfo::getBaseUri); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java index cec1894764..d5ea6c88de 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java @@ -33,37 +33,28 @@ package sonia.scm.api.rest.resources; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Function; import com.google.common.collect.Collections2; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.inject.Inject; - import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryTypePredicate; -import sonia.scm.util.HttpUtil; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import sonia.scm.template.Viewable; import javax.servlet.http.HttpServletRequest; - import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import sonia.scm.template.Viewable; +import java.io.IOException; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; /** * @@ -106,13 +97,12 @@ public class RepositoryRootResource @Produces(MediaType.TEXT_HTML) public Viewable renderRepositoriesRoot(@Context HttpServletRequest request, @PathParam("type") final String type) { - String baseUrl = HttpUtil.getCompleteUrl(request); //J- Collection unsortedRepositories = Collections2.transform( Collections2.filter( repositoryManager.getAll(), new RepositoryTypePredicate(type)) - , new RepositoryTransformFunction(baseUrl) + , new RepositoryTransformFunction() ); List repositories = Ordering.from( @@ -138,17 +128,9 @@ public class RepositoryRootResource public static class RepositoryTemplateElement { - /** - * Constructs ... - * - * - * @param repository - * @param baseUrl - */ - public RepositoryTemplateElement(Repository repository, String baseUrl) + public RepositoryTemplateElement(Repository repository) { this.repository = repository; - this.baseUrl = baseUrl; } //~--- get methods -------------------------------------------------------- @@ -175,22 +157,8 @@ public class RepositoryRootResource return repository; } - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return repository.createUrl(baseUrl); - } - //~--- fields ------------------------------------------------------------- - /** Field description */ - private String baseUrl; - /** Field description */ private Repository repository; @@ -236,31 +204,10 @@ public class RepositoryRootResource private static class RepositoryTransformFunction implements Function { - - public RepositoryTransformFunction(String baseUrl) - { - this.baseUrl = baseUrl; - } - - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override public RepositoryTemplateElement apply(Repository repository) { - return new RepositoryTemplateElement(repository, baseUrl); + return new RepositoryTemplateElement(repository); } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private String baseUrl; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index a600d1f45a..03b5728627 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -41,6 +41,6 @@ public class MapperModule extends AbstractModule { bind(UIPluginDtoMapper.class); bind(UIPluginDtoCollectionMapper.class); - bind(UriInfoStore.class).in(ServletScopes.REQUEST); + bind(ScmPathInfoStore.class).in(ServletScopes.REQUEST); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java index b79dd9a766..a4586fab36 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java @@ -24,7 +24,7 @@ public abstract class MeToUserDtoMapper extends UserToUserDtoMapper{ @AfterMapping - void appendLinks(User user, @MappingTarget UserDto target) { + protected void appendLinks(User user, @MappingTarget UserDto target) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); if (UserPermissions.delete(user).isPermitted()) { linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName()))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 90e9b3daed..2a587bc2fe 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -238,8 +238,9 @@ public class PermissionRootResource { * @throws RepositoryNotFoundException if the repository does not exists */ private Repository load(String namespace, String name) throws RepositoryNotFoundException { - return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) - .orElseThrow(() -> new RepositoryNotFoundException(name)); + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + return Optional.ofNullable(manager.get(namespaceAndName)) + .orElseThrow(() -> new RepositoryNotFoundException(namespaceAndName)); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index c597e12d4f..ddfe432d73 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -24,7 +24,7 @@ public class RepositoryDto extends HalRepresentation { @JsonInclude(JsonInclude.Include.NON_NULL) private Instant lastModified; private String namespace; - @Pattern(regexp = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$") + @Pattern(regexp = "^[A-z0-9\\-_]+$") private String name; private boolean archived = false; @NotEmpty diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index f76669fbb9..29a4107aad 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; +import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; @@ -11,9 +12,13 @@ import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.ScmProtocol; + +import java.util.List; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; +import static java.util.stream.Collectors.toList; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") @@ -30,7 +35,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper protocolLinks = repositoryService.getSupportedProtocols() + .map(this::createProtocolLink) + .collect(toList()); + linksBuilder.array(protocolLinks); + } if (repositoryService.isSupported(Command.TAGS)) { linksBuilder.single(link("tags", resourceLinks.tag().all(target.getNamespace(), target.getName()))); } @@ -50,4 +60,8 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper { } @AfterMapping - void appendLinks(User user, @MappingTarget UserDto target) { + protected void appendLinks(User user, @MappingTarget UserDto target) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName())); if (UserPermissions.delete(user).isPermitted()) { linksBuilder.single(link("delete", resourceLinks.user().delete(target.getName()))); diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java index 0d59d77027..de0d689c52 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java @@ -37,10 +37,8 @@ package sonia.scm.filter; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; - import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; - import sonia.scm.Priority; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; @@ -48,14 +46,15 @@ import sonia.scm.security.SecurityRequests; import sonia.scm.web.filter.HttpFilter; import sonia.scm.web.filter.SecurityHttpServletRequestWrapper; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -63,7 +62,8 @@ import javax.servlet.http.HttpServletResponse; */ @Priority(Filters.PRIORITY_AUTHORIZATION) // TODO find a better way for unprotected resources -@WebElement(value = "/api/rest/(?!v2/ui).*", regex = true) +@WebElement(value = REST_API_PATH + "" + + "/(?!v2/ui).*", regex = true) public class SecurityFilter extends HttpFilter { diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 02ae67719b..8cb325b818 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -31,10 +31,7 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.github.sdorra.ssp.PermissionActionCheck; -import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Inject; @@ -43,7 +40,6 @@ import org.apache.shiro.concurrent.SubjectAwareExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.AlreadyExistsException; -import sonia.scm.ArgumentIsInvalidException; import sonia.scm.ConfigurationException; import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; @@ -54,11 +50,9 @@ import sonia.scm.config.ScmConfiguration; import sonia.scm.security.KeyGenerator; import sonia.scm.util.AssertUtil; import sonia.scm.util.CollectionAppender; -import sonia.scm.util.HttpUtil; import sonia.scm.util.IOUtil; import sonia.scm.util.Util; -import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -71,8 +65,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; -//~--- JDK imports ------------------------------------------------------------ - /** * Default implementation of {@link RepositoryManager}. * @@ -90,7 +82,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private final KeyGenerator keyGenerator; private final RepositoryDAO repositoryDAO; private final Set types; - private RepositoryMatcher repositoryMatcher; private NamespaceStrategy namespaceStrategy; private final ManagerDaoAdapter managerDaoAdapter; @@ -99,12 +90,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { public DefaultRepositoryManager(ScmConfiguration configuration, SCMContextProvider contextProvider, KeyGenerator keyGenerator, RepositoryDAO repositoryDAO, Set handlerSet, - RepositoryMatcher repositoryMatcher, NamespaceStrategy namespaceStrategy) { this.configuration = configuration; this.keyGenerator = keyGenerator; this.repositoryDAO = repositoryDAO; - this.repositoryMatcher = repositoryMatcher; this.namespaceStrategy = namespaceStrategy; ThreadFactory factory = new ThreadFactoryBuilder() @@ -317,71 +306,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { return validTypes; } - @Override - public Repository getFromRequest(HttpServletRequest request) { - AssertUtil.assertIsNotNull(request); - - return getFromUri(HttpUtil.getStrippedURI(request)); - } - - @Override - public Repository getFromUri(String uri) { - AssertUtil.assertIsNotEmpty(uri); - - if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) { - uri = uri.substring(1); - } - - int typeSeparator = uri.indexOf(HttpUtil.SEPARATOR_PATH); - Repository repository = null; - - if (typeSeparator > 0) { - String type = uri.substring(0, typeSeparator); - - uri = uri.substring(typeSeparator + 1); - repository = getFromTypeAndUri(type, uri); - } - - return repository; - } - - private Repository getFromTypeAndUri(String type, String uri) { - if (Strings.isNullOrEmpty(type)) { - throw new ArgumentIsInvalidException("argument type is required"); - } - - if (Strings.isNullOrEmpty(uri)) { - throw new ArgumentIsInvalidException("argument uri is required"); - } - - // remove ;jsessionid, jetty bug? - uri = HttpUtil.removeMatrixParameter(uri); - - Repository repository = null; - - if (handlerMap.containsKey(type)) { - Collection repositories = repositoryDAO.getAll(); - - PermissionActionCheck check = RepositoryPermissions.read(); - - for (Repository r : repositories) { - if (repositoryMatcher.matches(r, type, uri)) { - check.check(r); - repository = r.clone(); - - break; - } - } - } - - if ((repository == null) && logger.isDebugEnabled()) { - logger.debug("could not find repository with type {} and uri {}", type, - uri); - } - - return repository; - } - @Override public RepositoryHandler getHandler(String type) { return handlerMap.get(type); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java index c3341bbd22..62067db172 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java @@ -33,86 +33,32 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.servlet.RequestScoped; -import sonia.scm.security.ScmSecurityException; - -//~--- JDK imports ------------------------------------------------------------ - import javax.servlet.http.HttpServletRequest; -/** - * - * @author Sebastian Sdorra - */ @RequestScoped -public class DefaultRepositoryProvider implements RepositoryProvider -{ +public class DefaultRepositoryProvider implements RepositoryProvider { - /** Field description */ public static final String ATTRIBUTE_NAME = "scm.request.repository"; - //~--- constructors --------------------------------------------------------- + private final Provider requestProvider; - /** - * Constructs ... - * - * - * @param requestProvider - * @param manager - */ @Inject - public DefaultRepositoryProvider( - Provider requestProvider, - RepositoryManager manager) - { + public DefaultRepositoryProvider(Provider requestProvider) { this.requestProvider = requestProvider; - this.manager = manager; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - * - * @throws ScmSecurityException - */ @Override - public Repository get() throws ScmSecurityException - { - Repository repository = null; + public Repository get() { HttpServletRequest request = requestProvider.get(); - if (request != null) - { - repository = (Repository) request.getAttribute(ATTRIBUTE_NAME); - - if (repository == null) - { - repository = manager.getFromRequest(request); - - if (repository != null) - { - request.setAttribute(ATTRIBUTE_NAME, repository); - } - } + if (request != null) { + return (Repository) request.getAttribute(ATTRIBUTE_NAME); } - return repository; + throw new IllegalStateException("request not found"); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final RepositoryManager manager; - - /** Field description */ - private final Provider requestProvider; } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java b/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java index 0d27c6d250..52dea4223b 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java @@ -61,8 +61,7 @@ public final class HealthChecker { Repository repository = repositoryManager.get(id); if (repository == null) { - throw new RepositoryNotFoundException( - "could not find repository with id ".concat(id)); + throw new RepositoryNotFoundException(id); } doCheck(repository); diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java index 225767cd3b..81bb2092c9 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java @@ -3,12 +3,14 @@ package sonia.scm.security; import javax.servlet.http.HttpServletRequest; import java.util.regex.Pattern; +import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; + /** * Created by masuewer on 04.07.18. */ public final class SecurityRequests { - private static final Pattern URI_LOGIN_PATTERN = Pattern.compile("/api/rest(?:/v2)?/auth/access_token"); + private static final Pattern URI_LOGIN_PATTERN = Pattern.compile(REST_API_PATH + "(?:/v2)?/auth/access_token"); private SecurityRequests() {} diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java new file mode 100644 index 0000000000..bc085d2cc7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -0,0 +1,79 @@ +package sonia.scm.web.protocol; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpStatus; +import sonia.scm.PushStateDispatcher; +import sonia.scm.filter.WebElement; +import sonia.scm.repository.DefaultRepositoryProvider; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.web.UserAgent; +import sonia.scm.web.UserAgentParser; + +import javax.inject.Provider; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +@Singleton +@WebElement(value = HttpProtocolServlet.PATTERN) +@Slf4j +public class HttpProtocolServlet extends HttpServlet { + + public static final String PATH = "/repo"; + public static final String PATTERN = PATH + "/*"; + + private final RepositoryServiceFactory serviceFactory; + + private final Provider requestProvider; + + private final PushStateDispatcher dispatcher; + private final UserAgentParser userAgentParser; + + + @Inject + public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, Provider requestProvider, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { + this.serviceFactory = serviceFactory; + this.requestProvider = requestProvider; + this.dispatcher = dispatcher; + this.userAgentParser = userAgentParser; + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + UserAgent userAgent = userAgentParser.parse(request); + if (userAgent.isBrowser()) { + log.trace("dispatch browser request for user agent {}", userAgent); + dispatcher.dispatch(request, response, request.getRequestURI()); + } else { + + String pathInfo = request.getPathInfo(); + Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(pathInfo); + if (namespaceAndName.isPresent()) { + service(request, response, namespaceAndName.get()); + } else { + log.debug("namespace and name not found in request path {}", pathInfo); + response.setStatus(HttpStatus.SC_BAD_REQUEST); + } + } + } + + private void service(HttpServletRequest req, HttpServletResponse resp, NamespaceAndName namespaceAndName) throws IOException, ServletException { + try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { + requestProvider.get().setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository()); + HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); + protocol.serve(req, resp, getServletConfig()); + } catch (RepositoryNotFoundException e) { + log.debug("Repository not found for namespace and name {}", namespaceAndName, e); + resp.setStatus(HttpStatus.SC_NOT_FOUND); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java new file mode 100644 index 0000000000..22e2433561 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java @@ -0,0 +1,41 @@ +package sonia.scm.web.protocol; + +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.util.HttpUtil; + +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +final class NamespaceAndNameFromPathExtractor { + + private NamespaceAndNameFromPathExtractor() {} + + static Optional fromUri(String uri) { + if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) { + uri = uri.substring(1); + } + + int endOfNamespace = uri.indexOf(HttpUtil.SEPARATOR_PATH); + if (endOfNamespace < 1) { + return empty(); + } + + String namespace = uri.substring(0, endOfNamespace); + int nameSeparatorIndex = uri.indexOf(HttpUtil.SEPARATOR_PATH, endOfNamespace + 1); + int nameIndex = nameSeparatorIndex > 0 ? nameSeparatorIndex : uri.length(); + if (nameIndex == endOfNamespace + 1) { + return empty(); + } + + String name = uri.substring(endOfNamespace + 1, nameIndex); + + int nameDotIndex = name.indexOf('.'); + if (nameDotIndex >= 0) { + return of(new NamespaceAndName(namespace, name.substring(0, nameDotIndex))); + } else { + return of(new NamespaceAndName(namespace, name)); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index c8d25ae82d..13ed9cddcd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -92,7 +93,7 @@ public class BranchRootResourceTest extends RepositoryTestBase { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); - super.branchRootResource = MockProvider.of(branchRootResource); + super.branchRootResource = Providers.of(branchRootResource); dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index e4e991a7d4..b80b62167b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -79,7 +80,7 @@ public class ChangesetRootResourceTest extends RepositoryTestBase { public void prepareEnvironment() throws Exception { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); - super.changesetRootResource = MockProvider.of(changesetRootResource); + super.changesetRootResource = Providers.of(changesetRootResource); dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java index f6a8fa4e09..fe22c944c4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -63,7 +64,7 @@ public class DiffResourceTest extends RepositoryTestBase { @Before public void prepareEnvironment() throws Exception { diffRootResource = new DiffRootResource(serviceFactory); - super.diffRootResource = MockProvider.of(diffRootResource); + super.diffRootResource = Providers.of(diffRootResource); dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java index 778682f62d..934e05d0d1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -81,7 +82,7 @@ public class FileHistoryResourceTest extends RepositoryTestBase { public void prepareEnvironment() throws Exception { fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper); - super.fileHistoryRootResource = MockProvider.of(fileHistoryRootResource); + super.fileHistoryRootResource = Providers.of(fileHistoryRootResource); dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java index 5b457ce4e9..5066f56ff7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java @@ -11,7 +11,6 @@ import org.junit.Test; import sonia.scm.PageResult; import sonia.scm.group.Group; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; @@ -28,9 +27,9 @@ import static sonia.scm.PageResult.createPage; public class GroupCollectionToDtoMapperTest { - private final UriInfo uriInfo = mock(UriInfo.class); - private final UriInfoStore uriInfoStore = new UriInfoStore(); - private final ResourceLinks resourceLinks = new ResourceLinks(uriInfoStore); + private final ScmPathInfo uriInfo = mock(ScmPathInfo.class); + private final ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore(); + private final ResourceLinks resourceLinks = new ResourceLinks(scmPathInfoStore); private final GroupToGroupDtoMapper groupToDtoMapper = mock(GroupToGroupDtoMapper.class); private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -41,10 +40,10 @@ public class GroupCollectionToDtoMapperTest { @Before public void init() throws URISyntaxException { - uriInfoStore.set(uriInfo); + scmPathInfoStore.set(uriInfo); URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(GroupRootResource.GROUPS_PATH_V2 + "/"); - when(uriInfo.getBaseUri()).thenReturn(baseUri); + when(uriInfo.getApiRestUri()).thenReturn(baseUri); subjectThreadState.bind(); ThreadContext.bind(subject); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 1e42016ace..f662542ff7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; @@ -73,7 +74,7 @@ public class GroupRootResourceTest { GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); GroupCollectionResource groupCollectionResource = new GroupCollectionResource(groupManager, dtoToGroupMapper, groupCollectionToDtoMapper, resourceLinks); GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper); - GroupRootResource groupRootResource = new GroupRootResource(MockProvider.of(groupCollectionResource), MockProvider.of(groupResource)); + GroupRootResource groupRootResource = new GroupRootResource(Providers.of(groupCollectionResource), Providers.of(groupResource)); dispatcher = createDispatcher(groupRootResource); dispatcher.getProviderFactory().registerProviderInstance(new JSONContextResolver(new ObjectMapperProvider().get())); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java index 37c584dfdf..c84e1a21a7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java @@ -4,7 +4,6 @@ import org.junit.Before; import org.junit.Test; import javax.ws.rs.Path; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; @@ -37,7 +36,7 @@ public class LinkBuilderTest { } } - private UriInfo uriInfo = mock(UriInfo.class); + private ScmPathInfo uriInfo = mock(ScmPathInfo.class); @Test public void shouldBuildSimplePath() { @@ -94,6 +93,6 @@ public class LinkBuilderTest { @Before public void setBaseUri() throws URISyntaxException { - when(uriInfo.getBaseUri()).thenReturn(new URI("http://example.com/")); + when(uriInfo.getApiRestUri()).thenReturn(new URI("http://example.com/")); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index b0109d86b7..1dc5049fd9 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -17,7 +17,6 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; @@ -45,9 +44,9 @@ public class MeResourceTest { private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock - private UriInfo uriInfo; + private ScmPathInfo uriInfo; @Mock - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @Mock private UserManager userManager; @@ -73,9 +72,9 @@ public class MeResourceTest { when(userManager.getDefaultType()).thenReturn("xml"); userToDtoMapper.setResourceLinks(resourceLinks); MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService); + when(uriInfo.getApiRestUri()).thenReturn(URI.create("/")); + when(scmPathInfoStore.get()).thenReturn(uriInfo); dispatcher = createDispatcher(meResource); - when(uriInfo.getBaseUri()).thenReturn(URI.create("/")); - when(uriInfoStore.get()).thenReturn(uriInfo); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockProvider.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockProvider.java deleted file mode 100644 index bf84e4fe15..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -package sonia.scm.api.v2.resources; - -import javax.inject.Provider; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * A mockito implementation of CDI {@link javax.inject.Provider}. - */ -class MockProvider { - - private MockProvider() {} - - static Provider of(I instance) { - @SuppressWarnings("unchecked") // Can't make mockito return typed provider - Provider provider = mock(Provider.class); - when(provider.get()).thenReturn(instance); - return provider; - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockScmProtocol.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockScmProtocol.java new file mode 100644 index 0000000000..1ae5344849 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockScmProtocol.java @@ -0,0 +1,23 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.repository.api.ScmProtocol; + +class MockScmProtocol implements ScmProtocol { + private final String type; + private final String protocol; + + public MockScmProtocol(String type, String protocol) { + this.type = type; + this.protocol = protocol; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getUrl() { + return protocol; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java index f8b555f180..64942fb84b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -71,7 +72,7 @@ public class ModificationsResourceTest extends RepositoryTestBase { @Before public void prepareEnvironment() throws Exception { modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper); - super.modificationsRootResource = MockProvider.of(modificationsRootResource); + super.modificationsRootResource = Providers.of(modificationsRootResource); dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index ac5ad07cf8..563e9bdbc0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableList; +import com.google.inject.util.Providers; import de.otto.edison.hal.HalRepresentation; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -137,7 +138,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { initMocks(this); permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); - super.permissionRootResource = MockProvider.of(permissionRootResource); + super.permissionRootResource = Providers.of(permissionRootResource); dispatcher = createDispatcher(getRepositoryRootResource()); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 7c6deedf65..0b2e50b6d9 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -3,13 +3,13 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -20,6 +20,7 @@ import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryIsNotArchivedException; import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; @@ -30,6 +31,7 @@ import java.net.URISyntaxException; import java.net.URL; import static java.util.Collections.singletonList; +import static java.util.stream.Stream.of; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; @@ -64,8 +66,14 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Mock private RepositoryManager repositoryManager; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + @Mock + private ScmPathInfoStore scmPathInfoStore; + @Mock + private ScmPathInfo uriInfo; private final URI baseUri = URI.create("/"); @@ -83,8 +91,11 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { super.dtoToRepositoryMapper = dtoToRepositoryMapper; super.manager = repositoryManager; RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); - super.repositoryCollectionResource = MockProvider.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks)); + super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks)); dispatcher = createDispatcher(getRepositoryRootResource()); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(scmPathInfoStore.get()).thenReturn(uriInfo); + when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); } @Test @@ -268,6 +279,20 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertFalse(modifiedRepositoryCaptor.getValue().getPermissions().isEmpty()); } + @Test + public void shouldCreateArrayOfProtocolUrls() throws Exception { + mockRepository("space", "repo"); + when(service.getSupportedProtocols()).thenReturn(of(new MockScmProtocol("http", "http://"), new MockScmProtocol("ssh", "ssh://"))); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"protocol\":[{\"href\":\"http://\",\"name\":\"http\"},{\"href\":\"ssh://\",\"name\":\"ssh\"}]")); + } + private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java index c3cc56958a..3d3b28ae51 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import sonia.scm.repository.RepositoryManager; import javax.inject.Provider; @@ -23,7 +24,7 @@ public abstract class RepositoryTestBase { RepositoryRootResource getRepositoryRootResource() { - return new RepositoryRootResource(MockProvider.of(new RepositoryResource( + return new RepositoryRootResource(Providers.of(new RepositoryResource( repositoryToDtoMapper, dtoToRepositoryMapper, manager, diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 0c77d40023..2e6048d6b8 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -7,7 +7,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; @@ -15,13 +14,17 @@ import sonia.scm.repository.Permission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.ScmProtocol; import java.net.URI; import static java.util.Collections.singletonList; +import static java.util.stream.Stream.of; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -39,8 +42,14 @@ public class RepositoryToRepositoryDtoMapperTest { private final URI baseUri = URI.create("http://example.com/base/"); @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService repositoryService; + @Mock + private ScmPathInfoStore scmPathInfoStore; + @Mock + private ScmPathInfo uriInfo; @InjectMocks private RepositoryToRepositoryDtoMapperImpl mapper; @@ -48,7 +57,11 @@ public class RepositoryToRepositoryDtoMapperTest { @Before public void init() { initMocks(this); - when(serviceFactory.create(any(Repository.class)).isSupported(any(Command.class))).thenReturn(true); + when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); + when(repositoryService.isSupported(any(Command.class))).thenReturn(true); + when(repositoryService.getSupportedProtocols()).thenReturn(of()); + when(scmPathInfoStore.get()).thenReturn(uriInfo); + when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); } @After @@ -129,14 +142,14 @@ public class RepositoryToRepositoryDtoMapperTest { @Test public void shouldNotCreateTagsLink_ifNotSupported() { - when(serviceFactory.create(any(Repository.class)).isSupported(Command.TAGS)).thenReturn(false); + when(repositoryService.isSupported(Command.TAGS)).thenReturn(false); RepositoryDto dto = mapper.map(createTestRepository()); assertFalse(dto.getLinks().getLinkBy("tags").isPresent()); } @Test public void shouldNotCreateBranchesLink_ifNotSupported() { - when(serviceFactory.create(any(Repository.class)).isSupported(Command.BRANCHES)).thenReturn(false); + when(repositoryService.isSupported(Command.BRANCHES)).thenReturn(false); RepositoryDto dto = mapper.map(createTestRepository()); assertFalse(dto.getLinks().getLinkBy("branches").isPresent()); } @@ -165,6 +178,43 @@ public class RepositoryToRepositoryDtoMapperTest { dto.getLinks().getLinkBy("permissions").get().getHref()); } + @Test + public void shouldCreateCorrectProtocolLinks() { + when(repositoryService.getSupportedProtocols()).thenReturn( + of(mockProtocol("http", "http://scm"), mockProtocol("other", "some://protocol")) + ); + + RepositoryDto dto = mapper.map(createTestRepository()); + assertTrue("should contain http link", dto.getLinks().stream().anyMatch(l -> l.getName().equals("http") && l.getHref().equals("http://scm"))); + assertTrue("should contain other link", dto.getLinks().stream().anyMatch(l -> l.getName().equals("other") && l.getHref().equals("some://protocol"))); + } + + @Test + @SubjectAware(username = "community") + public void shouldCreateProtocolLinksForPullPermission() { + when(repositoryService.getSupportedProtocols()).thenReturn( + of(mockProtocol("http", "http://scm"), mockProtocol("other", "some://protocol")) + ); + + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals(2, dto.getLinks().getLinksBy("protocol").size()); + } + + @Test + @SubjectAware(username = "unpriv") + public void shouldNotCreateProtocolLinksWithoutPullPermission() { + when(repositoryService.getSupportedProtocols()).thenReturn( + of(mockProtocol("http", "http://scm"), mockProtocol("other", "some://protocol")) + ); + + RepositoryDto dto = mapper.map(createTestRepository()); + assertTrue(dto.getLinks().getLinksBy("protocol").isEmpty()); + } + + private ScmProtocol mockProtocol(String type, String protocol) { + return new MockScmProtocol(type, protocol); + } + private Repository createTestRepository() { Repository repository = new Repository(); repository.setNamespace("testspace"); @@ -179,4 +229,5 @@ public class RepositoryToRepositoryDtoMapperTest { return repository; } + } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java index 9adca13225..2476785d70 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; @@ -22,8 +23,10 @@ import java.util.List; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_OK; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) @@ -52,7 +55,7 @@ public class RepositoryTypeRootResourceTest { RepositoryTypeCollectionToDtoMapper collectionMapper = new RepositoryTypeCollectionToDtoMapper(mapper, resourceLinks); RepositoryTypeCollectionResource collectionResource = new RepositoryTypeCollectionResource(repositoryManager, collectionMapper); RepositoryTypeResource resource = new RepositoryTypeResource(repositoryManager, mapper); - RepositoryTypeRootResource rootResource = new RepositoryTypeRootResource(MockProvider.of(collectionResource), MockProvider.of(resource)); + RepositoryTypeRootResource rootResource = new RepositoryTypeRootResource(Providers.of(collectionResource), Providers.of(resource)); dispatcher.getRegistry().addSingletonResource(rootResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 5e5897eb2a..c70510fe39 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import javax.ws.rs.core.UriInfo; import java.net.URI; import static org.mockito.Mockito.mock; @@ -10,8 +9,8 @@ public class ResourceLinksMock { public static ResourceLinks createMock(URI baseUri) { ResourceLinks resourceLinks = mock(ResourceLinks.class); - UriInfo uriInfo = mock(UriInfo.class); - when(uriInfo.getBaseUri()).thenReturn(baseUri); + ScmPathInfo uriInfo = mock(ScmPathInfo.class); + when(uriInfo.getApiRestUri()).thenReturn(baseUri); ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo); when(resourceLinks.user()).thenReturn(userLinks); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index a9209828c3..0544bf6a0d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -6,7 +6,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.NamespaceAndName; -import javax.ws.rs.core.UriInfo; import java.net.URI; import static org.junit.Assert.assertEquals; @@ -18,9 +17,9 @@ public class ResourceLinksTest { private static final String BASE_URL = "http://example.com/"; @Mock - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @Mock - private UriInfo uriInfo; + private ScmPathInfo uriInfo; @InjectMocks private ResourceLinks resourceLinks; @@ -177,7 +176,7 @@ public class ResourceLinksTest { @Before public void initUriInfo() { initMocks(this); - when(uriInfoStore.get()).thenReturn(uriInfo); - when(uriInfo.getBaseUri()).thenReturn(URI.create(BASE_URL)); + when(scmPathInfoStore.get()).thenReturn(uriInfo); + when(uriInfo.getApiRestUri()).thenReturn(URI.create(BASE_URL)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmPathInfoStoreTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmPathInfoStoreTest.java new file mode 100644 index 0000000000..544a918b8b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmPathInfoStoreTest.java @@ -0,0 +1,35 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ScmPathInfoStoreTest { + + @Test + public void shouldReturnSetInfo() { + URI someUri = URI.create("/anything"); + + ScmPathInfo uriInfo = mock(ScmPathInfo.class); + ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore(); + + when(uriInfo.getApiRestUri()).thenReturn(someUri); + + scmPathInfoStore.set(uriInfo); + + assertSame(someUri, scmPathInfoStore.get().getApiRestUri()); + } + + @Test(expected = IllegalStateException.class) + public void shouldFailIfSetTwice() { + ScmPathInfo uriInfo = mock(ScmPathInfo.class); + ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore(); + + scmPathInfoStore.set(uriInfo); + scmPathInfoStore.set(uriInfo); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 4759e1ebd7..c84a74bc92 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; @@ -63,7 +64,7 @@ public class SourceRootResourceTest extends RepositoryTestBase { when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto); SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper); - super.sourceRootResource = MockProvider.of(sourceRootResource); + super.sourceRootResource = Providers.of(sourceRootResource); dispatcher = createDispatcher(getRepositoryRootResource()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java index 3149182d98..5f49f31183 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -71,7 +72,7 @@ public class TagRootResourceTest extends RepositoryTestBase { public void prepareEnvironment() throws Exception { tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper); tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper); - super.tagRootResource = MockProvider.of(tagRootResource); + super.tagRootResource = Providers.of(tagRootResource); dispatcher = createDispatcher(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UriInfoStoreTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UriInfoStoreTest.java deleted file mode 100644 index 559e701ae3..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UriInfoStoreTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package sonia.scm.api.v2.resources; - -import org.junit.Test; - -import javax.ws.rs.core.UriInfo; - -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; - -public class UriInfoStoreTest { - - @Test - public void shouldReturnSetInfo() { - UriInfo uriInfo = mock(UriInfo.class); - UriInfoStore uriInfoStore = new UriInfoStore(); - - uriInfoStore.set(uriInfo); - - assertSame(uriInfo, uriInfoStore.get()); - } - - @Test(expected = IllegalStateException.class) - public void shouldFailIfSetTwice() { - UriInfo uriInfo = mock(UriInfo.class); - UriInfoStore uriInfoStore = new UriInfoStore(); - - uriInfoStore.set(uriInfo); - uriInfoStore.set(uriInfo); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 35cd755a5d..2cd1e01989 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; +import com.google.inject.util.Providers; import org.apache.shiro.authc.credential.PasswordService; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; @@ -78,8 +79,8 @@ public class UserRootResourceTest { UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper, userCollectionToDtoMapper, resourceLinks, passwordService); UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService); - UserRootResource userRootResource = new UserRootResource(MockProvider.of(userCollectionResource), - MockProvider.of(userResource)); + UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource), + Providers.of(userResource)); dispatcher = createDispatcher(userRootResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java index 2452afb909..c55a33c39a 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java @@ -36,33 +36,38 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; import com.google.common.base.Charsets; +import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.UniformInterfaceException; import org.apache.shiro.crypto.hash.Sha256Hash; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.api.v2.resources.RepositoryDto; +import sonia.scm.api.v2.resources.UserDto; +import sonia.scm.api.v2.resources.UserToUserDtoMapperImpl; import sonia.scm.repository.PermissionType; -import sonia.scm.repository.Repository; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import sonia.scm.util.HttpUtil; +import sonia.scm.web.VndMediaType; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.io.IOException; +import java.net.URI; import java.util.UUID; import static org.junit.Assert.assertArrayEquals; import static sonia.scm.it.IntegrationTestUtil.BASE_URL; import static sonia.scm.it.IntegrationTestUtil.REST_BASE_URL; import static sonia.scm.it.IntegrationTestUtil.createAdminClient; +import static sonia.scm.it.IntegrationTestUtil.createResource; import static sonia.scm.it.IntegrationTestUtil.readJson; import static sonia.scm.it.RepositoryITUtil.createRepository; import static sonia.scm.it.RepositoryITUtil.deleteRepository; @@ -111,7 +116,6 @@ public class GitLfsITCase { } @Test - @Ignore("permissions not yet implemented") public void testLfsAPIWithOwnerPermissions() throws IOException { uploadAndDownloadAsUser(PermissionType.OWNER); } @@ -122,9 +126,11 @@ public class GitLfsITCase { createUser(trillian); try { - // TODO enable when permissions are implemented in v2 -// repository.getPermissions().add(new Permission(trillian.getId(), permissionType)); -// modifyRepository(repository); + String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); + IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) + .accept("*/*") + .type(VndMediaType.PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"WRITE\"}"); ScmClient client = new ScmClient(trillian.getId(), "secret123"); @@ -135,25 +141,27 @@ public class GitLfsITCase { } @Test - @Ignore("permissions not yet implemented") public void testLfsAPIWithWritePermissions() throws IOException { uploadAndDownloadAsUser(PermissionType.WRITE); } private void createUser(User user) { - adminClient.resource(REST_BASE_URL + "users.json").post(user); - } - - private void modifyRepository(Repository repository) { - adminClient.resource(REST_BASE_URL + "repositories/" + repository.getId() + ".json").put(repository); + UserDto dto = new UserToUserDtoMapperImpl(){ + @Override + protected void appendLinks(User user, UserDto target) {} + }.map(user); + dto.setPassword(user.getPassword()); + createResource(adminClient, "users") + .accept("*/*") + .type(VndMediaType.USER) + .post(ClientResponse.class, dto); } private void removeUser(User user) { - adminClient.resource(REST_BASE_URL + "users/" + user.getId() + ".json").delete(); + adminClient.resource(REST_BASE_URL + "users/" + user.getId()).delete(); } @Test - @Ignore("permissions not yet implemented") public void testLfsAPIWithoutWritePermissions() throws IOException { User trillian = UserTestData.createTrillian(); trillian.setPassword("secret123"); @@ -164,9 +172,11 @@ public class GitLfsITCase { try { - // TODO enable when permissions are implemented in v2 -// repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ)); -// modifyRepository(repository); + String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); + IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) + .accept("*/*") + .type(VndMediaType.PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); ScmClient client = new ScmClient(trillian.getId(), "secret123"); uploadAndDownload(client); @@ -176,7 +186,6 @@ public class GitLfsITCase { } @Test - @Ignore("permissions not yet implemented") public void testLfsDownloadWithReadPermissions() throws IOException { User trillian = UserTestData.createTrillian(); trillian.setPassword("secret123"); @@ -184,9 +193,11 @@ public class GitLfsITCase { try { - // TODO enable when permissions are implemented in v2 -// repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ)); -// modifyRepository(repository); + String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); + IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) + .accept("*/*") + .type(VndMediaType.PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); // upload data as admin String data = UUID.randomUUID().toString(); @@ -221,7 +232,7 @@ public class GitLfsITCase { LfsResponseBody response = request(client, request); String uploadURL = response.objects[0].actions.upload.href; - client.resource(uploadURL).put(data); + client.resource(uploadURL).header(HttpUtil.HEADER_USERAGENT, "git-lfs/z").put(data); return lfsObject; } @@ -233,14 +244,14 @@ public class GitLfsITCase { String json = client .resource(batchUrl) .accept("application/vnd.git-lfs+json") + .header(HttpUtil.HEADER_USERAGENT, "git-lfs/z") .header("Content-Type", "application/vnd.git-lfs+json") .post(String.class, requestAsString); return new ObjectMapperProvider().get().readValue(json, LfsResponseBody.class); } private String createBatchUrl() { - String url = BASE_URL + "git/" + repository.getNamespace() + "/" + repository.getName(); - return url + "/info/lfs/objects/batch"; + return String.format("%srepo/%s/%s/info/lfs/objects/batch", BASE_URL, repository.getNamespace(), repository.getName()); } private byte[] download(ScmClient client, LfsObject lfsObject) throws IOException { @@ -248,7 +259,7 @@ public class GitLfsITCase { LfsResponseBody response = request(client, request); String downloadUrl = response.objects[0].actions.download.href; - return client.resource(downloadUrl).get(byte[].class); + return client.resource(downloadUrl).header(HttpUtil.HEADER_USERAGENT, "git-lfs/z").get(byte[].class); } private LfsObject createLfsObject(byte[] data) { diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java index 13cae15907..9e75857f08 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java @@ -100,7 +100,7 @@ public class GitRepositoryPathMatcherITCase { // tests end private String createUrl() { - return BASE_URL + "git/" + repository.getNamespace() + "/" + repository.getName(); + return BASE_URL + "repo/" + repository.getNamespace() + "/" + repository.getName(); } private void cloneAndPush( String url ) throws IOException { diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java index dae80cd00d..d74769bf39 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java @@ -172,7 +172,7 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase Thread.sleep(WAIT_TIME); // check debug servlet that only one commit is present - WebResource.Builder wr = createResource(client, "../debug/" + repository.getNamespace() + "/" + repository.getName() + "/post-receive/last"); + WebResource.Builder wr = createResource(client, String.format("../debug/%s/%s/post-receive/last", repository.getNamespace(), repository.getName())); DebugHookData data = wr.get(DebugHookData.class); assertNotNull(data); assertThat(data.getChangesets(), allOf( @@ -195,8 +195,8 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase private RepositoryClient createRepositoryClient() throws IOException { - return REPOSITORY_CLIENT_FACTORY.create(repositoryType, - IntegrationTestUtil.BASE_URL + repositoryType + "/" + repository.getNamespace() + "/" + repository.getName(), + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, + String.format("%srepo/%s/%s", IntegrationTestUtil.BASE_URL, repository.getNamespace(), repository.getName()), IntegrationTestUtil.ADMIN_USERNAME, IntegrationTestUtil.ADMIN_PASSWORD, workingCopy ); } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index 5f4ea2fa72..448c2561f3 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -54,7 +54,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.SCMContextProvider; -import sonia.scm.Type; import sonia.scm.cache.GuavaCacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.security.AuthorizationCollector; @@ -120,7 +119,6 @@ public class DefaultRepositoryManagerPerfTest { keyGenerator, repositoryDAO, handlerSet, - repositoryMatcher, namespaceStrategy ); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index efd3f673b0..b7d231cf38 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -61,7 +61,6 @@ import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.Stack; @@ -383,69 +382,6 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { assertEquals("default_namespace", repository.getNamespace()); } - @Test - public void getRepositoryFromRequestUri_withoutLeadingSlash() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("scm-test", m.getFromUri("hg/namespace/scm-test").getName()); - assertEquals("namespace", m.getFromUri("hg/namespace/scm-test").getNamespace()); - } - - @Test - public void getRepositoryFromRequestUri_withLeadingSlash() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("scm-test", m.getFromUri("/hg/namespace/scm-test").getName()); - assertEquals("namespace", m.getFromUri("/hg/namespace/scm-test").getNamespace()); - } - - @Test - public void getRepositoryFromRequestUri_withPartialName() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("scm", m.getFromUri("hg/namespace/scm").getName()); - assertEquals("namespace", m.getFromUri("hg/namespace/scm").getNamespace()); - } - - @Test - public void getRepositoryFromRequestUri_withTrailingFilePath() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("test-1", m.getFromUri("/git/namespace/test-1/ka/some/path").getName()); - } - - @Test - public void getRepositoryFromRequestUri_forNotExistingRepositoryName() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertNull(m.getFromUri("/git/namespace/test-3/ka/some/path")); - } - - @Test - public void getRepositoryFromRequestUri_forWrongNamespace() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertNull(m.getFromUri("/git/other/other/test-2")); - } - @Test public void shouldSetNamespace() throws AlreadyExistsException { Repository repository = new Repository(null, "hg", null, "scm"); @@ -504,7 +440,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); return new DefaultRepositoryManager(configuration, contextProvider, - keyGenerator, repositoryDAO, handlerSet, createRepositoryMatcher(), namespaceStrategy); + keyGenerator, repositoryDAO, handlerSet, namespaceStrategy); } private void createRepository(RepositoryManager m, Repository repository) throws AlreadyExistsException { @@ -530,10 +466,6 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { assertEquals(repo.getLastModified(), other.getLastModified()); } - private RepositoryMatcher createRepositoryMatcher() { - return new RepositoryMatcher(Collections.emptySet()); - } - private Repository createRepository(Repository repository) throws AlreadyExistsException { manager.create(repository); assertNotNull(repository.getId()); diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java new file mode 100644 index 0000000000..077020f60c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -0,0 +1,114 @@ +package sonia.scm.web.protocol; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import sonia.scm.PushStateDispatcher; +import sonia.scm.repository.DefaultRepositoryProvider; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.web.UserAgent; +import sonia.scm.web.UserAgentParser; + +import javax.inject.Provider; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class HttpProtocolServletTest { + + + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private HttpServletRequest httpServletRequest; + @Mock + private PushStateDispatcher dispatcher; + @Mock + private UserAgentParser userAgentParser; + @Mock + private Provider requestProvider; + + @InjectMocks + private HttpProtocolServlet servlet; + + @Mock + private RepositoryService repositoryService; + @Mock + private UserAgent userAgent; + + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private HttpScmProtocol protocol; + + @Before + public void init() throws RepositoryNotFoundException { + initMocks(this); + when(userAgentParser.parse(request)).thenReturn(userAgent); + when(userAgent.isBrowser()).thenReturn(false); + NamespaceAndName existingRepo = new NamespaceAndName("space", "repo"); + when(serviceFactory.create(not(eq(existingRepo)))).thenThrow(RepositoryNotFoundException.class); + when(serviceFactory.create(existingRepo)).thenReturn(repositoryService); + when(requestProvider.get()).thenReturn(httpServletRequest); + } + + @Test + public void shouldDispatchBrowserRequests() throws ServletException, IOException { + when(userAgent.isBrowser()).thenReturn(true); + when(request.getRequestURI()).thenReturn("uri"); + + servlet.service(request, response); + + verify(dispatcher).dispatch(request, response, "uri"); + } + + @Test + public void shouldHandleBadPaths() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/illegal"); + + servlet.service(request, response); + + verify(response).setStatus(400); + } + + @Test + public void shouldHandleNotExistingRepository() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/not/exists"); + + servlet.service(request, response); + + verify(response).setStatus(404); + } + + @Test + public void shouldDelegateToProvider() throws RepositoryNotFoundException, IOException, ServletException { + when(request.getPathInfo()).thenReturn("/space/name"); + NamespaceAndName namespaceAndName = new NamespaceAndName("space", "name"); + doReturn(repositoryService).when(serviceFactory).create(namespaceAndName); + Repository repository = new Repository(); + when(repositoryService.getRepository()).thenReturn(repository); + when(repositoryService.getProtocol(HttpScmProtocol.class)).thenReturn(protocol); + + servlet.service(request, response); + + verify(httpServletRequest).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); + verify(protocol).serve(request, response, null); + verify(repositoryService).close(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java new file mode 100644 index 0000000000..0998010069 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java @@ -0,0 +1,68 @@ +package sonia.scm.web.protocol; + +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import sonia.scm.repository.NamespaceAndName; + +import java.util.Optional; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +public class NamespaceAndNameFromPathExtractorTest { + @TestFactory + Stream shouldExtractCorrectNamespaceAndName() { + return Stream.of( + "/space/repo", + "/space/repo/", + "/space/repo/here", + "/space/repo/here/there", + "space/repo", + "space/repo/", + "space/repo/here/there" + ).map(this::createCorrectTest); + } + + @TestFactory + Stream shouldHandleTrailingDotSomethings() { + return Stream.of( + "/space/repo.git", + "/space/repo.and.more", + "/space/repo." + ).map(this::createCorrectTest); + } + + private DynamicTest createCorrectTest(String path) { + return dynamicTest( + "should extract correct namespace and name for path " + path, + () -> { + Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + + assertThat(namespaceAndName.get()).isEqualTo(new NamespaceAndName("space", "repo")); + } + ); + } + + @TestFactory + Stream shouldHandleMissingParts() { + return Stream.of( + "", + "/", + "/space", + "/space/" + ).map(this::createFailureTest); + } + + private DynamicTest createFailureTest(String path) { + return dynamicTest( + "should not fail for wrong path " + path, + () -> { + Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + + assertThat(namespaceAndName.isPresent()).isFalse(); + } + ); + } +} diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini index 5073bf398d..9a39a2d46c 100644 --- a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini +++ b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini @@ -3,9 +3,11 @@ trillian = secret, admin dent = secret, creator, heartOfGold, puzzle42 unpriv = secret crato = secret, creator +community = secret, oss [roles] admin = * creator = repository:create heartOfGold = "repository:read,modify,delete:hof" puzzle42 = "repository:read,write:p42" +oss = "repository:pull"