merge with develop

This commit is contained in:
Eduard Heimbuch
2020-08-07 13:06:53 +02:00
15 changed files with 547 additions and 125 deletions

View File

@@ -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

View File

@@ -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.

55
docs/en/import.md Normal file
View File

@@ -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
```

View File

@@ -2,6 +2,7 @@
entries:
- /installation/
- /migrate-scm-manager-from-v1/
- /import/
- /faq/
- /known-issues/

View File

@@ -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();
}
}

View File

@@ -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<HttpScmProtocol> {
private final Provider<? extends ScmProviderHttpServlet> delegateProvider;
private final Provider<ScmPathInfoStore> pathInfoStore;
private final ScmConfiguration scmConfiguration;
private final Supplier<String> 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<? extends ScmProviderHttpServlet> delegateProvider, Provider<ScmPathInfoStore> 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<? extends ScmProviderHttpServlet> 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<String> {
private Optional<String> getPathFromScmPathInfoIfAvailable() {
try {
ScmPathInfoStore scmPathInfoStore = pathInfoStore.get();
if (scmPathInfoStore != null && scmPathInfoStore.get() != null) {
return of(scmPathInfoStore.get().getRootUri().toASCIIString());
private final Provider<ScmPathInfoStore> pathInfoStore;
private final ScmConfiguration scmConfiguration;
private LegacySupplier(Provider<ScmPathInfoStore> pathInfoStore, ScmConfiguration scmConfiguration) {
this.pathInfoStore = pathInfoStore;
this.scmConfiguration = scmConfiguration;
}
@Override
public String get() {
return getPathFromScmPathInfoIfAvailable().orElse(getPathFromConfiguration());
}
private Optional<String> 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 {

View File

@@ -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);

View File

@@ -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<ScmPathInfoStore> 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<ScmPathInfoStore> 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<ScmPathInfo> mockSetPathInfo() {
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/"));
}
private OngoingStubbing<ScmPathInfo> mockSetPathInfo() {
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/"));
}
}

View File

@@ -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
*

View File

@@ -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<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) {
super(servletProvider, uriInfoStore, scmConfiguration);
public GitScmProtocolProviderWrapper(ScmGitServletProvider servletProvider, RootURL rootURL) {
super(servletProvider, rootURL);
}
@Override

View File

@@ -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<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) {
super(servletProvider, uriInfoStore, scmConfiguration);
public HgScmProtocolProviderWrapper(HgCGIServletProvider servletProvider, RootURL rootURL) {
super(servletProvider, rootURL);
}
@Override

View File

@@ -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<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) {
super(servletProvider, uriInfoStore, scmConfiguration);
}
@Override
protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException {
super.initializeServlet(new SvnConfigEnhancer(config), httpServlet);
}

View File

@@ -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<HttpServletRequest> requestProvider;
private final ScmConfiguration configuration;
@Inject
public DefaultRootURL(Provider<HttpServletRequest> 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<String> 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;
}
}
}

View File

@@ -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 <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {

View File

@@ -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<HttpServletRequest> 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");
}
}