Merge pull request #1276 from scm-manager/bugfix/protocol_url_outofscope

Avoid stacktrace logging when protocol url is accessed outside of request scope
This commit is contained in:
René Pfeuffer
2020-08-04 10:41:32 +02:00
committed by GitHub
12 changed files with 484 additions and 124 deletions

View File

@@ -5,11 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## Unreleased
### Added
- New api to resolve SCM-Manager root url ([#1276](https://github.com/scm-manager/scm-manager/pull/1276))
### Changed ### Changed
- Help tooltips are now mutliline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271)) - Help tooltips are now mutliline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271))
### Fixed ### 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 ## [2.3.0] - 2020-07-23
### Added ### Added

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 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import sonia.scm.RootURL;
import sonia.scm.api.v2.resources.ScmPathInfoStore; import sonia.scm.api.v2.resources.ScmPathInfoStore;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
@@ -37,6 +38,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier;
import static java.util.Optional.empty; import static java.util.Optional.empty;
import static java.util.Optional.of; import static java.util.Optional.of;
@@ -45,16 +47,36 @@ import static java.util.Optional.of;
public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolProvider<HttpScmProtocol> { public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolProvider<HttpScmProtocol> {
private final Provider<? extends ScmProviderHttpServlet> delegateProvider; private final Provider<? extends ScmProviderHttpServlet> delegateProvider;
private final Provider<ScmPathInfoStore> pathInfoStore; private final Supplier<String> basePathSupplier;
private final ScmConfiguration scmConfiguration;
private volatile boolean isInitialized = false; 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) { protected InitializingHttpScmProtocolWrapper(Provider<? extends ScmProviderHttpServlet> delegateProvider, Provider<ScmPathInfoStore> pathInfoStore, ScmConfiguration scmConfiguration) {
this.delegateProvider = delegateProvider; this.delegateProvider = delegateProvider;
this.pathInfoStore = pathInfoStore; this.basePathSupplier = new LegacySupplier(pathInfoStore, scmConfiguration);
this.scmConfiguration = 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 { protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException {
@@ -64,30 +86,45 @@ public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolP
@Override @Override
public HttpScmProtocol get(Repository repository) { public HttpScmProtocol get(Repository repository) {
if (!repository.getType().equals(getType())) { 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() { private static class LegacySupplier implements Supplier<String> {
return getPathFromScmPathInfoIfAvailable().orElse(getPathFromConfiguration());
}
private Optional<String> getPathFromScmPathInfoIfAvailable() { private final Provider<ScmPathInfoStore> pathInfoStore;
try { private final ScmConfiguration scmConfiguration;
ScmPathInfoStore scmPathInfoStore = pathInfoStore.get();
if (scmPathInfoStore != null && scmPathInfoStore.get() != null) { private LegacySupplier(Provider<ScmPathInfoStore> pathInfoStore, ScmConfiguration scmConfiguration) {
return of(scmPathInfoStore.get().getRootUri().toASCIIString()); 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) { return empty();
log.debug("could not get ScmPathInfoStore from context", e); }
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 { 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 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.util; package sonia.scm.util;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
@@ -925,11 +925,16 @@ public final class HttpUtil
@VisibleForTesting @VisibleForTesting
static String createForwardedBaseUrl(HttpServletRequest request) static String createForwardedBaseUrl(HttpServletRequest request)
{ {
String proto = getHeader(request, HEADER_X_FORWARDED_PROTO, String fhost = getHeader(request, HEADER_X_FORWARDED_HOST, null);
request.getScheme()); 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 host;
String fhost = getHeader(request, HEADER_X_FORWARDED_HOST,
request.getScheme());
String port = request.getHeader(HEADER_X_FORWARDED_PORT); String port = request.getHeader(HEADER_X_FORWARDED_PORT);
int s = fhost.indexOf(SEPARATOR_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 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import com.google.inject.ProvisionException; import com.google.inject.ProvisionException;
import com.google.inject.util.Providers; import com.google.inject.util.Providers;
import org.junit.Before; import org.junit.jupiter.api.BeforeEach;
import org.junit.Test; 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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.OngoingStubbing; import org.mockito.stubbing.OngoingStubbing;
import sonia.scm.RootURL;
import sonia.scm.api.v2.resources.ScmPathInfo; import sonia.scm.api.v2.resources.ScmPathInfo;
import sonia.scm.api.v2.resources.ScmPathInfoStore; import sonia.scm.api.v2.resources.ScmPathInfoStore;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
@@ -44,101 +48,135 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock; import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class InitializingHttpScmProtocolWrapperTest { @ExtendWith(MockitoExtension.class)
class InitializingHttpScmProtocolWrapperTest {
private static final Repository REPOSITORY = new Repository("", "git", "space", "name"); private static final Repository REPOSITORY = new Repository("", "git", "space", "name");
@Mock @Mock
private ScmProviderHttpServlet delegateServlet; 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; 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) { @Nested
@Override class WithRootURL {
public String getType() {
return "git"; @Mock
} private RootURL rootURL;
};
when(scmConfiguration.getBaseUrl()).thenReturn("http://example.com/scm"); @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 @Nested
public void shouldUsePathFromPathInfo() { class WithPathInfoStore {
mockSetPathInfo();
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 @Mock
public void shouldUseConfigurationWhenPathInfoNotSet() { private HttpServletRequest request;
HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); @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 wrapper = new InitializingHttpScmProtocolWrapper(Providers.of(delegateServlet), pathInfoStoreProvider, scmConfiguration) {
public void shouldUseConfigurationWhenNotInRequestScope() { @Override
when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test")); 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 assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
public void shouldInitializeAndDelegateRequestThroughFilter() throws ServletException, IOException { }
HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
httpScmProtocol.serve(request, response, servletConfig); @Test
void shouldUseConfigurationWhenPathInfoNotSet() {
HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
verify(delegateServlet).init(servletConfig); assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
verify(delegateServlet).service(request, response, REPOSITORY); }
}
@Test @Test
public void shouldInitializeOnlyOnce() throws ServletException, IOException { void shouldUseConfigurationWhenNotInRequestScope() {
HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test"));
httpScmProtocol.serve(request, response, servletConfig); HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY);
httpScmProtocol.serve(request, response, servletConfig);
verify(delegateServlet, times(1)).init(servletConfig); assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl());
verify(delegateServlet, times(2)).service(request, response, REPOSITORY); }
}
@Test(expected = IllegalArgumentException.class) @Test
public void shouldFailForIllegalScmType() { void shouldInitializeAndDelegateRequestThroughFilter() throws ServletException, IOException {
HttpScmProtocol httpScmProtocol = wrapper.get(new Repository("", "other", "space", "name")); 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 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.util; package sonia.scm.util;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
@@ -234,6 +234,12 @@ public class HttpUtilTest
HttpUtil.createForwardedBaseUrl(request)); HttpUtil.createForwardedBaseUrl(request));
} }
@Test(expected = IllegalStateException.class)
public void shouldTrowIllegalStateExceptionWithoutForwardedHostHeader() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpUtil.createForwardedBaseUrl(request);
}
/** /**
* Method description * Method description
* *

View File

@@ -21,25 +21,23 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.web; package sonia.scm.web;
import sonia.scm.api.v2.resources.ScmPathInfoStore; import sonia.scm.RootURL;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton; import javax.inject.Singleton;
@Singleton @Singleton
@Extension @Extension
public class GitScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { public class GitScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper {
@Inject @Inject
public GitScmProtocolProviderWrapper(ScmGitServletProvider servletProvider, Provider<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) { public GitScmProtocolProviderWrapper(ScmGitServletProvider servletProvider, RootURL rootURL) {
super(servletProvider, uriInfoStore, scmConfiguration); super(servletProvider, rootURL);
} }
@Override @Override

View File

@@ -21,25 +21,24 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.web; package sonia.scm.web;
import sonia.scm.api.v2.resources.ScmPathInfoStore; import sonia.scm.RootURL;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.HgRepositoryHandler;
import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton; import javax.inject.Singleton;
@Singleton @Singleton
@Extension @Extension
public class HgScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { public class HgScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper {
@Inject @Inject
public HgScmProtocolProviderWrapper(HgCGIServletProvider servletProvider, Provider<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) { public HgScmProtocolProviderWrapper(HgCGIServletProvider servletProvider, RootURL rootURL) {
super(servletProvider, uriInfoStore, scmConfiguration); super(servletProvider, rootURL);
} }
@Override @Override

View File

@@ -21,18 +21,16 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.web; package sonia.scm.web;
import sonia.scm.api.v2.resources.ScmPathInfoStore; import sonia.scm.RootURL;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.repository.SvnRepositoryHandler;
import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper;
import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.repository.spi.ScmProviderHttpServlet;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton; import javax.inject.Singleton;
import javax.servlet.ServletConfig; import javax.servlet.ServletConfig;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
@@ -45,19 +43,18 @@ public class SvnScmProtocolProviderWrapper extends InitializingHttpScmProtocolWr
public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath";
@Inject
public SvnScmProtocolProviderWrapper(SvnDAVServletProvider servletProvider, RootURL rootURL) {
super(servletProvider, rootURL);
}
@Override @Override
public String getType() { public String getType() {
return SvnRepositoryHandler.TYPE_NAME; return SvnRepositoryHandler.TYPE_NAME;
} }
@Inject
public SvnScmProtocolProviderWrapper(SvnDAVServletProvider servletProvider, Provider<ScmPathInfoStore> uriInfoStore, ScmConfiguration scmConfiguration) {
super(servletProvider, uriInfoStore, scmConfiguration);
}
@Override @Override
protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException { protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException {
super.initializeServlet(new SvnConfigEnhancer(config), httpServlet); 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.Default; import sonia.scm.Default;
import sonia.scm.DefaultRootURL;
import sonia.scm.PushStateDispatcher; import sonia.scm.PushStateDispatcher;
import sonia.scm.PushStateDispatcherProvider; import sonia.scm.PushStateDispatcherProvider;
import sonia.scm.RootURL;
import sonia.scm.Undecorated; import sonia.scm.Undecorated;
import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.api.rest.ObjectMapperProvider;
import sonia.scm.api.v2.resources.BranchLinkProvider; import sonia.scm.api.v2.resources.BranchLinkProvider;
@@ -239,6 +241,9 @@ class ScmServletModule extends ServletModule {
// bind api link provider // bind api link provider
bind(BranchLinkProvider.class).to(DefaultBranchLinkProvider.class); 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) { 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");
}
}