From 5bf49179143cd38f10a9f71f6a87d96448639e09 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 3 Aug 2020 13:37:16 +0200 Subject: [PATCH] adds RootURL which resolves the scm root url regardless of the guice scope --- scm-core/src/main/java/sonia/scm/RootURL.java | 53 +++++++ .../main/java/sonia/scm/DefaultRootURL.java | 84 +++++++++++ .../java/sonia/scm/DefaultRootURLTest.java | 134 ++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 scm-core/src/main/java/sonia/scm/RootURL.java create mode 100644 scm-webapp/src/main/java/sonia/scm/DefaultRootURL.java create mode 100644 scm-webapp/src/test/java/sonia/scm/DefaultRootURLTest.java 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-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/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"); + } + +}