diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4e4195a2..727b24d991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Anonymous mode for the web ui ([#1284](https://github.com/scm-manager/scm-manager/pull/1284)) +## [2.3.1] - 2020-08-04 +### Added +- New api to resolve SCM-Manager root url ([#1276](https://github.com/scm-manager/scm-manager/pull/1276)) + ### Changed -- Help tooltips are now mutliline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271)) +- Help tooltips are now multiline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271)) ### Fixed -- Fixed unecessary horizontal scrollbar in modal dialogs ([#1271](https://github.com/scm-manager/scm-manager/pull/1271)) +- Fixed unnecessary horizontal scrollbar in modal dialogs ([#1271](https://github.com/scm-manager/scm-manager/pull/1271)) +- Avoid stacktrace logging when protocol url is accessed outside of request scope ([#1276](https://github.com/scm-manager/scm-manager/pull/1276)) ## [2.3.0] - 2020-07-23 ### Added @@ -254,3 +259,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [2.1.1]: https://www.scm-manager.org/download/2.1.1 [2.2.0]: https://www.scm-manager.org/download/2.2.0 [2.3.0]: https://www.scm-manager.org/download/2.3.0 +[2.3.1]: https://www.scm-manager.org/download/2.3.1 diff --git a/docs/en/faq.md b/docs/en/faq.md index ef64b4d9db..64a8957395 100644 --- a/docs/en/faq.md +++ b/docs/en/faq.md @@ -40,3 +40,7 @@ After changing the configuration, SCM-Manager must be restarted. ### How do I install plugins? Find the plugin you like to install at [plugins](/plugins#categories) and follow the installation instructions on the install page of the plugin. + +### How can I import my existing (git|mercurial|subversion) repository + +Please have a look on [these](../import/) detailed instructions. diff --git a/docs/en/import.md b/docs/en/import.md new file mode 100644 index 0000000000..fd4d9e6d99 --- /dev/null +++ b/docs/en/import.md @@ -0,0 +1,55 @@ +--- +title: Import existing repositories +subtitle: How to import existing repositories into SCM-Manager +displayToc: true +--- + +## Git + +First you have to clone the old repository with the `mirror` option. +This option ensures that all branches and tags are fetched from the remote repository. +Assuming that your remote repository is accessible under the url `https://hgttg.com/r/git/heart-of-gold`, the clone command should look like this: + +```bash +git clone --mirror https://hgttg.com/r/git/heart-of-gold +``` + +Than you have to create your new repository via the SCM-Manager web interface and copy the url. +In this example we assume that the new repository is available at `https://hitchhiker.com/scm/repo/hgttg/heart-of-gold`. After the new repository is created, we can configure our local repository for the new location and push all refs. + +```bash +cd heart-of-gold +git remote set-url origin https://hitchhiker.com/scm/repo/hgttg/heart-of-gold +git push --mirror +``` + +## Mercurial + +To import an existing mercurial repository, we have to create a new repository over the SCM-Manager web interface, clone it, pull from the old repository and push to the new repository. +In this example we assume that the old repository is `https://hgttg.com/r/hg/heart-of-gold` and the newly created is located at `https://hitchhiker.com/scm/repo/hgttg/heart-of-gold`: + +```bash +hg clone https://hitchhiker.com/scm/repo/hgttg/heart-of-gold +cd heart-of-gold +hg pull https://hgttg.com/r/hg/heart-of-gold +hg push +``` + +## Subversion + +Subversion is not as easy as mercurial or git. +For subversion we have to locate the old repository on the filesystem and create a dump with the `svnadmin` tool. + +```bash +svnadmin dump /path/to/repo > oldrepo.dump +``` + +Now we have to create a new repository via the SCM-Manager web interface. +After the repository is created, we have to find its location on the filesystem. +This could be done by finding the directory with the newest timestamp in your scm home directory under `repositories`. +You can check whether you have found the correct directory by having a look at the file `metadata.xml`. Here you should find the namespace and the name of the repository created. +Now its time to import the dump from the old repository: + +```bash +svnadmin load /path/to/scm-home/repositories/id/data < oldrepo.dump +``` diff --git a/docs/en/navigation.yml b/docs/en/navigation.yml index e696c98bc6..9c30b03156 100644 --- a/docs/en/navigation.yml +++ b/docs/en/navigation.yml @@ -2,6 +2,7 @@ entries: - /installation/ - /migrate-scm-manager-from-v1/ + - /import/ - /faq/ - /known-issues/ diff --git a/scm-core/src/main/java/sonia/scm/RootURL.java b/scm-core/src/main/java/sonia/scm/RootURL.java new file mode 100644 index 0000000000..0187b51fb1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/RootURL.java @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm; + +import java.net.URL; + +/** + * RootURL is able to return the root url of the SCM-Manager instance, + * regardless of the scope (web request, async hook, ssh command, etc). + * + * @since 2.3.1 + */ +public interface RootURL { + + /** + * Returns the root url of the SCM-Manager instance. + * + * @return root url + */ + URL get(); + + /** + * Returns the root url of the SCM-Manager instance as string. + * + * @return root url as string + */ + default String getAsString() { + return get().toExternalForm(); + } + +} 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 index 063c7de408..c6a656f276 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java @@ -21,10 +21,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import lombok.extern.slf4j.Slf4j; +import sonia.scm.RootURL; import sonia.scm.api.v2.resources.ScmPathInfoStore; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.Repository; @@ -37,6 +38,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Optional; +import java.util.function.Supplier; import static java.util.Optional.empty; import static java.util.Optional.of; @@ -45,16 +47,36 @@ import static java.util.Optional.of; public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolProvider { private final Provider delegateProvider; - private final Provider pathInfoStore; - private final ScmConfiguration scmConfiguration; + private final Supplier basePathSupplier; private volatile boolean isInitialized = false; - + /** + * Constructs a new {@link InitializingHttpScmProtocolWrapper}. + * + * @param delegateProvider injection provider for the servlet delegate + * @param pathInfoStore url info store + * @param scmConfiguration scm-manager main configuration + * + * @deprecated use {@link InitializingHttpScmProtocolWrapper(Provider, RootURL)} instead. + */ + @Deprecated protected InitializingHttpScmProtocolWrapper(Provider delegateProvider, Provider pathInfoStore, ScmConfiguration scmConfiguration) { this.delegateProvider = delegateProvider; - this.pathInfoStore = pathInfoStore; - this.scmConfiguration = scmConfiguration; + this.basePathSupplier = new LegacySupplier(pathInfoStore, scmConfiguration); + } + + /** + * Constructs a new {@link InitializingHttpScmProtocolWrapper}. + * + * @param delegateProvider injection provider for the servlet delegate + * @param rootURL root url + * + * @since 2.3.1 + */ + public InitializingHttpScmProtocolWrapper(Provider delegateProvider, RootURL rootURL) { + this.delegateProvider = delegateProvider; + this.basePathSupplier = rootURL::getAsString; } protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException { @@ -64,30 +86,45 @@ public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolP @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())); + throw new IllegalArgumentException( + String.format("cannot handle repository with type %s with protocol for type %s", repository.getType(), getType()) + ); } - return new ProtocolWrapper(repository, computeBasePath()); + return new ProtocolWrapper(repository, basePathSupplier.get()); } - private String computeBasePath() { - return getPathFromScmPathInfoIfAvailable().orElse(getPathFromConfiguration()); - } + private static class LegacySupplier implements Supplier { - private Optional getPathFromScmPathInfoIfAvailable() { - try { - ScmPathInfoStore scmPathInfoStore = pathInfoStore.get(); - if (scmPathInfoStore != null && scmPathInfoStore.get() != null) { - return of(scmPathInfoStore.get().getRootUri().toASCIIString()); + private final Provider pathInfoStore; + private final ScmConfiguration scmConfiguration; + + private LegacySupplier(Provider pathInfoStore, ScmConfiguration scmConfiguration) { + this.pathInfoStore = pathInfoStore; + this.scmConfiguration = scmConfiguration; + } + + @Override + public String get() { + 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); } - } 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(); } - return empty(); - } - private String getPathFromConfiguration() { - log.debug("using base path from configuration: {}", scmConfiguration.getBaseUrl()); - return scmConfiguration.getBaseUrl(); } private class ProtocolWrapper extends HttpScmProtocol { diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index 88adc2a24c..7056911a20 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.util; //~--- non-JDK imports -------------------------------------------------------- @@ -925,11 +925,16 @@ public final class HttpUtil @VisibleForTesting static String createForwardedBaseUrl(HttpServletRequest request) { - String proto = getHeader(request, HEADER_X_FORWARDED_PROTO, - request.getScheme()); + String fhost = getHeader(request, HEADER_X_FORWARDED_HOST, null); + if (fhost == null) { + throw new IllegalStateException( + String.format("request has no %s header and does not look like it is forwarded", HEADER_X_FORWARDED_HOST) + ); + } + + String proto = getHeader(request, HEADER_X_FORWARDED_PROTO, request.getScheme()); String host; - String fhost = getHeader(request, HEADER_X_FORWARDED_HOST, - request.getScheme()); + String port = request.getHeader(HEADER_X_FORWARDED_PORT); int s = fhost.indexOf(SEPARATOR_PORT); 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 index 8c1a0c8057..f450eb380a 100644 --- a/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java @@ -21,15 +21,19 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + 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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.OngoingStubbing; +import sonia.scm.RootURL; import sonia.scm.api.v2.resources.ScmPathInfo; import sonia.scm.api.v2.resources.ScmPathInfoStore; import sonia.scm.config.ScmConfiguration; @@ -44,101 +48,135 @@ 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; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.*; -public class InitializingHttpScmProtocolWrapperTest { +@ExtendWith(MockitoExtension.class) +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"); + @Nested + class WithRootURL { + + @Mock + private RootURL rootURL; + + @BeforeEach + void init() { + wrapper = new InitializingHttpScmProtocolWrapper(Providers.of(delegateServlet), rootURL) { + @Override + public String getType() { + return "git"; + } + }; + when(rootURL.getAsString()).thenReturn("https://hitchhiker.com/scm"); + } + + @Test + void shouldReturnUrlFromRootURL() { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + assertEquals("https://hitchhiker.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } + } - @Test - public void shouldUsePathFromPathInfo() { - mockSetPathInfo(); + @Nested + class WithPathInfoStore { - HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + @Mock + private ScmPathInfoStore pathInfoStore; + @Mock + private ScmConfiguration scmConfiguration; - assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); - } + private Provider pathInfoStoreProvider; - @Test - public void shouldUseConfigurationWhenPathInfoNotSet() { - HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private ServletConfig servletConfig; - assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); - } + @BeforeEach + void init() { + pathInfoStoreProvider = mock(Provider.class); + lenient().when(pathInfoStoreProvider.get()).thenReturn(pathInfoStore); - @Test - public void shouldUseConfigurationWhenNotInRequestScope() { - when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test")); + wrapper = new InitializingHttpScmProtocolWrapper(Providers.of(delegateServlet), pathInfoStoreProvider, scmConfiguration) { + @Override + public String getType() { + return "git"; + } + }; + lenient().when(scmConfiguration.getBaseUrl()).thenReturn("http://example.com/scm"); + } - HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + @Test + void shouldUsePathFromPathInfo() { + mockSetPathInfo(); - assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); - } + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); - @Test - public void shouldInitializeAndDelegateRequestThroughFilter() throws ServletException, IOException { - HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } - httpScmProtocol.serve(request, response, servletConfig); + @Test + void shouldUseConfigurationWhenPathInfoNotSet() { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); - verify(delegateServlet).init(servletConfig); - verify(delegateServlet).service(request, response, REPOSITORY); - } + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } - @Test - public void shouldInitializeOnlyOnce() throws ServletException, IOException { - HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + @Test + void shouldUseConfigurationWhenNotInRequestScope() { + when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test")); - httpScmProtocol.serve(request, response, servletConfig); - httpScmProtocol.serve(request, response, servletConfig); + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); - verify(delegateServlet, times(1)).init(servletConfig); - verify(delegateServlet, times(2)).service(request, response, REPOSITORY); - } + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } - @Test(expected = IllegalArgumentException.class) - public void shouldFailForIllegalScmType() { - HttpScmProtocol httpScmProtocol = wrapper.get(new Repository("", "other", "space", "name")); - } + @Test + 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 + 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 + void shouldFailForIllegalScmType() { + Repository repository = new Repository("", "other", "space", "name"); + assertThrows( + IllegalArgumentException.class, + () -> wrapper.get(repository) + ); + } + + private OngoingStubbing mockSetPathInfo() { + return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/")); + } - private OngoingStubbing mockSetPathInfo() { - return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/")); } } diff --git a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java index 162033c7ca..40aa184145 100644 --- a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.util; //~--- non-JDK imports -------------------------------------------------------- @@ -234,6 +234,12 @@ public class HttpUtilTest HttpUtil.createForwardedBaseUrl(request)); } + @Test(expected = IllegalStateException.class) + public void shouldTrowIllegalStateExceptionWithoutForwardedHostHeader() { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpUtil.createForwardedBaseUrl(request); + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitScmProtocolProviderWrapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitScmProtocolProviderWrapper.java index 87adbd65fe..318282d7bb 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitScmProtocolProviderWrapper.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitScmProtocolProviderWrapper.java @@ -21,25 +21,23 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; -import sonia.scm.api.v2.resources.ScmPathInfoStore; -import sonia.scm.config.ScmConfiguration; +import sonia.scm.RootURL; import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; import javax.inject.Inject; -import javax.inject.Provider; import javax.inject.Singleton; @Singleton @Extension public class GitScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { @Inject - public GitScmProtocolProviderWrapper(ScmGitServletProvider servletProvider, Provider uriInfoStore, ScmConfiguration scmConfiguration) { - super(servletProvider, uriInfoStore, scmConfiguration); + public GitScmProtocolProviderWrapper(ScmGitServletProvider servletProvider, RootURL rootURL) { + super(servletProvider, rootURL); } @Override 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 index 28f80952e2..12b85a1746 100644 --- 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 @@ -21,25 +21,24 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; -import sonia.scm.api.v2.resources.ScmPathInfoStore; -import sonia.scm.config.ScmConfiguration; +import sonia.scm.RootURL; 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); + public HgScmProtocolProviderWrapper(HgCGIServletProvider servletProvider, RootURL rootURL) { + super(servletProvider, rootURL); } @Override 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 index a8e8857066..fbe71d4bb2 100644 --- 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 @@ -21,18 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; -import sonia.scm.api.v2.resources.ScmPathInfoStore; -import sonia.scm.config.ScmConfiguration; +import sonia.scm.RootURL; 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; @@ -45,19 +43,18 @@ public class SvnScmProtocolProviderWrapper extends InitializingHttpScmProtocolWr public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; + @Inject + public SvnScmProtocolProviderWrapper(SvnDAVServletProvider servletProvider, RootURL rootURL) { + super(servletProvider, rootURL); + } + @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); } diff --git a/scm-webapp/src/main/java/sonia/scm/DefaultRootURL.java b/scm-webapp/src/main/java/sonia/scm/DefaultRootURL.java new file mode 100644 index 0000000000..fb21e7b6f0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/DefaultRootURL.java @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm; + +import com.google.inject.OutOfScopeException; +import com.google.inject.ProvisionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Optional; + +/** + * Default implementation of {@link RootURL}. + * + * @since 2.3.1 + */ +public class DefaultRootURL implements RootURL { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultRootURL.class); + + private final Provider requestProvider; + private final ScmConfiguration configuration; + + @Inject + public DefaultRootURL(Provider requestProvider, ScmConfiguration configuration) { + this.requestProvider = requestProvider; + this.configuration = configuration; + } + + @Override + public URL get() { + String url = fromRequest().orElse(configuration.getBaseUrl()); + if (url == null) { + throw new IllegalStateException("The configured base url is empty. This can only happened if SCM-Manager has not received any requests."); + } + try { + return new URL(url); + } catch (MalformedURLException e) { + throw new IllegalStateException(String.format("base url \"%s\" is malformed", url), e); + } + } + + private Optional fromRequest() { + try { + HttpServletRequest request = requestProvider.get(); + return Optional.of(HttpUtil.getCompleteUrl(request)); + } catch (ProvisionException ex) { + if (ex.getCause() instanceof OutOfScopeException) { + LOG.debug("could not find request, fall back to base url from configuration"); + return Optional.empty(); + } + throw ex; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java index ea87ba0b4f..ec7499aa02 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java @@ -33,8 +33,10 @@ import com.google.inject.throwingproviders.ThrowingProviderBinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.Default; +import sonia.scm.DefaultRootURL; import sonia.scm.PushStateDispatcher; import sonia.scm.PushStateDispatcherProvider; +import sonia.scm.RootURL; import sonia.scm.Undecorated; import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.api.v2.resources.BranchLinkProvider; @@ -239,6 +241,9 @@ class ScmServletModule extends ServletModule { // bind api link provider bind(BranchLinkProvider.class).to(DefaultBranchLinkProvider.class); + + // bind url helper + bind(RootURL.class).to(DefaultRootURL.class); } private void bind(Class clazz, Class defaultImplementation) { diff --git a/scm-webapp/src/test/java/sonia/scm/DefaultRootURLTest.java b/scm-webapp/src/test/java/sonia/scm/DefaultRootURLTest.java new file mode 100644 index 0000000000..4190c6288a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/DefaultRootURLTest.java @@ -0,0 +1,134 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm; + +import com.google.inject.OutOfScopeException; +import com.google.inject.ProvisionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.util.HttpUtil; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultRootURLTest { + + private static final String URL_CONFIG = "https://hitchhiker.com/from-configuration"; + private static final String URL_REQUEST = "https://hitchhiker.com/from-request"; + + @Mock + private Provider requestProvider; + + @Mock + private HttpServletRequest request; + + private ScmConfiguration configuration; + + private RootURL rootURL; + + @BeforeEach + void init() { + configuration = new ScmConfiguration(); + rootURL = new DefaultRootURL(requestProvider, configuration); + } + + @Test + void shouldUseRootURLFromRequest() { + bindRequestUrl(); + assertThat(rootURL.getAsString()).isEqualTo(URL_REQUEST); + } + + private void bindRequestUrl() { + when(requestProvider.get()).thenReturn(request); + when(request.getRequestURL()).thenReturn(new StringBuffer(URL_REQUEST)); + when(request.getRequestURI()).thenReturn("/from-request"); + when(request.getContextPath()).thenReturn("/from-request"); + } + + @Test + void shouldUseRootURLFromConfiguration() { + bindNonHttpScope(); + configuration.setBaseUrl(URL_CONFIG); + assertThat(rootURL.getAsString()).isEqualTo(URL_CONFIG); + } + + private void bindNonHttpScope() { + when(requestProvider.get()).thenThrow( + new ProvisionException("no request available", new OutOfScopeException("out of scope")) + ); + } + + @Test + void shouldThrowNonOutOfScopeProvisioningExceptions() { + when(requestProvider.get()).thenThrow( + new ProvisionException("something ugly happened", new IllegalStateException("some wrong state")) + ); + + assertThrows(ProvisionException.class, () -> rootURL.get()); + } + + @Test + void shouldThrowIllegalStateExceptionForMalformedBaseUrl() { + bindNonHttpScope(); + configuration.setBaseUrl("non_url"); + + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> rootURL.get()); + assertThat(exception.getMessage()).contains("malformed", "non_url"); + assertThat(exception.getCause()).isInstanceOf(MalformedURLException.class); + } + + @Test + void shouldThrowIllegalStateExceptionIfBaseURLIsNotConfigured() { + bindNonHttpScope(); + + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> rootURL.get()); + assertThat(exception.getMessage()).contains("empty"); + } + + @Test + void shouldUseRootURLFromForwardedRequest() { + bindForwardedRequestUrl(); + assertThat(rootURL.get()).hasHost("hitchhiker.com"); + } + + private void bindForwardedRequestUrl() { + when(requestProvider.get()).thenReturn(request); + when(request.getHeader(HttpUtil.HEADER_X_FORWARDED_HOST)).thenReturn("hitchhiker.com"); + when(request.getScheme()).thenReturn("https"); + when(request.getServerPort()).thenReturn(443); + when(request.getContextPath()).thenReturn("/from-request"); + } + +}