diff --git a/CHANGELOG.md b/CHANGELOG.md index 42479c8a5d..35cda5ed44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,17 @@ 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). ## Unreleased + ### Added - Extension point to add links to the repository cards from plug ins ([#1041](https://github.com/scm-manager/scm-manager/pull/1041)) +### Changed +- Update resteasy to version 4.5.2.Final +- Use browser built-in EventSource for apiClient subscriptions + +### Removed +- EventSource Polyfill + ### Fixed - Build on windows ([#1048](https://github.com/scm-manager/scm-manager/issues/1048), [#1049](https://github.com/scm-manager/scm-manager/issues/1049), [#1056](https://github.com/scm-manager/scm-manager/pull/1056)) - Show specific notification for plugin actions on plugin administration ([#1057](https://github.com/scm-manager/scm-manager/pull/1057)) diff --git a/pom.xml b/pom.xml index 4fe6c4f475..72023b2ca3 100644 --- a/pom.xml +++ b/pom.xml @@ -254,6 +254,17 @@ ${resteasy.version} + + + io.smallrye.config + smallrye-config + 1.6.2 + + javax.ws.rs javax.ws.rs-api @@ -837,7 +848,7 @@ 3.0.1 2.1.1 - 4.4.2.Final + 4.5.2.Final 1.19.4 2.10.2 4.2.2 diff --git a/scm-core/src/main/java/sonia/scm/security/BearerToken.java b/scm-core/src/main/java/sonia/scm/security/BearerToken.java index 43a225b5b6..f42147fbe2 100644 --- a/scm-core/src/main/java/sonia/scm/security/BearerToken.java +++ b/scm-core/src/main/java/sonia/scm/security/BearerToken.java @@ -96,17 +96,13 @@ public final class BearerToken implements AuthenticationToken { /** * Creates a new {@link BearerToken} from raw string representation for the given ui session id. * - * @param sessionId session id of the client + * @param session session id of the client * @param rawToken bearer token string representation * * @return new bearer token */ - public static BearerToken create(@Nullable String sessionId, String rawToken) { + public static BearerToken create(@Nullable SessionId session, String rawToken) { Preconditions.checkArgument(!Strings.isNullOrEmpty(rawToken), "raw token is required"); - SessionId session = null; - if (!Strings.isNullOrEmpty(sessionId)) { - session = SessionId.valueOf(sessionId); - } return new BearerToken(session, rawToken); } } diff --git a/scm-core/src/main/java/sonia/scm/security/SessionId.java b/scm-core/src/main/java/sonia/scm/security/SessionId.java index 3c4fb126ff..7d1b657bfb 100644 --- a/scm-core/src/main/java/sonia/scm/security/SessionId.java +++ b/scm-core/src/main/java/sonia/scm/security/SessionId.java @@ -1,14 +1,23 @@ package sonia.scm.security; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import lombok.EqualsAndHashCode; +import sonia.scm.util.HttpUtil; -import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.Optional; /** * Client side session id. */ -public final class SessionId { +@EqualsAndHashCode +public final class SessionId implements Serializable { + + @VisibleForTesting + public static final String PARAMETER = "X-SCM-Session-ID"; private final String value; @@ -16,24 +25,15 @@ public final class SessionId { this.value = value; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SessionId sessionID = (SessionId) o; - return Objects.equals(value, sessionID.value); - } - - @Override - public int hashCode() { - return Objects.hash(value); - } - @Override public String toString() { return value; } + public static Optional from(HttpServletRequest request) { + return HttpUtil.getHeaderOrGetParameter(request, PARAMETER).map(SessionId::valueOf); + } + public static SessionId valueOf(String value) { Preconditions.checkArgument(!Strings.isNullOrEmpty(value), "session id could not be empty or null"); return new SessionId(value); 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 8edc0c8e14..820f492b8f 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -37,7 +37,6 @@ package sonia.scm.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; -import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,6 +51,7 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Arrays; import java.util.Locale; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -116,12 +116,6 @@ public final class HttpUtil */ public static final String HEADER_SCM_CLIENT = "X-SCM-Client"; - /** - * header for identifying the scm-manager client session - * @since 2.0.0 - */ - public static final String HEADER_SCM_SESSION = "X-SCM-Session-ID"; - /** Field description */ public static final String HEADER_USERAGENT = "User-Agent"; @@ -830,6 +824,24 @@ public final class HttpUtil return uri; } + /** + * Returns header value or query parameter if the request is a get request. + * + * @param request http request + * @param parameter name of header/parameter + * + * @return header value or query parameter + * + * @since 2.0.0 + */ + public static Optional getHeaderOrGetParameter(HttpServletRequest request, String parameter) { + String value = request.getHeader(parameter); + if (Strings.isNullOrEmpty(value) && "GET".equalsIgnoreCase(request.getMethod())) { + value = request.getParameter(parameter); + } + return Optional.ofNullable(value); + } + /** * Returns the given uri without leading separator. * @@ -882,16 +894,14 @@ public final class HttpUtil /** * Returns true if the http request is send by the scm-manager web interface. * - * * @param request http request * * @return true if the request comes from the web interface. * @since 1.19 */ - public static boolean isWUIRequest(HttpServletRequest request) - { - return SCM_CLIENT_WUI.equalsIgnoreCase( - request.getHeader(HEADER_SCM_CLIENT)); + public static boolean isWUIRequest(HttpServletRequest request) { + Optional client = getHeaderOrGetParameter(request, HEADER_SCM_CLIENT); + return client.isPresent() && SCM_CLIENT_WUI.equalsIgnoreCase(client.get()); } //~--- methods -------------------------------------------------------------- diff --git a/scm-core/src/test/java/sonia/scm/security/SessionIdTest.java b/scm-core/src/test/java/sonia/scm/security/SessionIdTest.java new file mode 100644 index 0000000000..89bcf5f594 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/security/SessionIdTest.java @@ -0,0 +1,42 @@ +package sonia.scm.security; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.servlet.http.HttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SessionIdTest { + + @Mock + private HttpServletRequest request; + + @Test + void shouldReturnSessionIdFromHeader() { + when(request.getHeader(SessionId.PARAMETER)).thenReturn("abc42"); + + assertThat(SessionId.from(request)).contains(SessionId.valueOf("abc42")); + } + + @Test + void shouldReturnSessionIdFromQueryParameter() { + when(request.getMethod()).thenReturn("GET"); + when(request.getParameter(SessionId.PARAMETER)).thenReturn("abc42"); + + assertThat(SessionId.from(request)).contains(SessionId.valueOf("abc42")); + } + + @Test + void shouldReturnSessionIdFromQueryParameterOnlyForGetRequest() { + when(request.getMethod()).thenReturn("POST"); + lenient().when(request.getParameter(SessionId.PARAMETER)).thenReturn("abc42"); + + assertThat(SessionId.from(request)).isEmpty(); + } +} 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 c827764a92..8e2804a749 100644 --- a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java @@ -38,7 +38,9 @@ package sonia.scm.util; import org.junit.Test; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.SessionId; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -466,4 +468,47 @@ public class HttpUtilTest assertEquals("test/two/three", HttpUtil.getUriWithoutStartSeperator("test/two/three")); } + + @Test + public void testGetHeaderOrGetParameterWithHeader() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Domain")).thenReturn("hitchhiker"); + + assertThat(HttpUtil.getHeaderOrGetParameter(request, "Domain")).contains("hitchhiker"); + } + + @Test + public void testGetHeaderOrGetParameterWithParameter() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getParameter("Domain")).thenReturn("hitchhiker"); + + assertThat(HttpUtil.getHeaderOrGetParameter(request, "Domain")).contains("hitchhiker"); + } + + @Test + public void testGetHeaderOrGetParameterOnPost() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("POST"); + lenient().when(request.getParameter("Domain")).thenReturn("hitchhiker"); + + assertThat(HttpUtil.getHeaderOrGetParameter(request, "Domain")).isEmpty(); + } + + @Test + public void testIsWUIRequestWithHeader() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); + + assertThat(HttpUtil.isWUIRequest(request)).isTrue(); + } + + @Test + public void testIsWUIRequestWithParameter() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getParameter(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); + + assertThat(HttpUtil.isWUIRequest(request)).isTrue(); + } } diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 3864805706..e60bbfdea3 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -49,7 +49,6 @@ "@scm-manager/ui-types": "^2.0.0-SNAPSHOT", "classnames": "^2.2.6", "date-fns": "^2.4.1", - "event-source-polyfill": "^1.0.9", "gitdiff-parser": "^0.1.2", "lowlight": "^1.13.0", "query-string": "5", diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts index c8990b000c..f7e32f46ef 100644 --- a/scm-ui/ui-components/src/apiclient.ts +++ b/scm-ui/ui-components/src/apiclient.ts @@ -1,6 +1,4 @@ import { contextPath } from "./urls"; -// @ts-ignore we have not types for event-source-polyfill -import { EventSourcePolyfill } from "event-source-polyfill"; import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError, BackendErrorContent } from "./errors"; type SubscriptionEvent = { @@ -124,6 +122,10 @@ export function createUrl(url: string) { return `${contextPath}/api/v2${urlWithStartingSlash}`; } +export function createUrlWithIdentifiers(url: string): string { + return createUrl(url) + "?X-SCM-Client=WUI&X-SCM-Session-ID=" + sessionId; +} + class ApiClient { get(url: string): Promise { return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure); @@ -218,9 +220,8 @@ class ApiClient { } subscribe(url: string, argument: SubscriptionArgument): Cancel { - const es = new EventSourcePolyfill(createUrl(url), { - withCredentials: true, - headers: createRequestHeaders() + const es = new EventSource(createUrlWithIdentifiers(url), { + withCredentials: true }); let listeners: MessageListeners; @@ -228,9 +229,11 @@ class ApiClient { if ("onMessage" in argument) { listeners = (argument as SubscriptionContext).onMessage; if (argument.onError) { + // @ts-ignore typing of EventSource is weird es.onerror = argument.onError; } if (argument.onOpen) { + // @ts-ignore typing of EventSource is weird es.onopen = argument.onOpen; } } else { @@ -238,6 +241,7 @@ class ApiClient { } for (const type in listeners) { + // @ts-ignore typing of EventSource is weird es.addEventListener(type, listeners[type]); } diff --git a/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java index 4bb79e7b9a..21330c6e17 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java @@ -35,6 +35,7 @@ package sonia.scm.web; import sonia.scm.plugin.Extension; import sonia.scm.security.BearerToken; +import sonia.scm.security.SessionId; import sonia.scm.util.HttpUtil; //~--- JDK imports ------------------------------------------------------------ @@ -68,10 +69,8 @@ public class BearerWebTokenGenerator extends SchemeBasedWebTokenGenerator { BearerToken token = null; - if (HttpUtil.AUTHORIZATION_SCHEME_BEARER.equalsIgnoreCase(scheme)) - { - String sessionId = request.getHeader(HttpUtil.HEADER_SCM_SESSION); - token = BearerToken.create(sessionId, authorization); + if (HttpUtil.AUTHORIZATION_SCHEME_BEARER.equalsIgnoreCase(scheme)) { + token = BearerToken.create(SessionId.from(request).orElse(null), authorization); } return token; diff --git a/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java index 2a0a97c07e..a6b2a16296 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java @@ -40,6 +40,8 @@ import sonia.scm.security.BearerToken; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; + +import sonia.scm.security.SessionId; import sonia.scm.util.HttpUtil; /** @@ -62,19 +64,14 @@ public class CookieBearerWebTokenGenerator implements WebTokenGenerator * @return {@link BearerToken} or {@code null} */ @Override - public BearerToken createToken(HttpServletRequest request) - { + public BearerToken createToken(HttpServletRequest request) { BearerToken token = null; Cookie[] cookies = request.getCookies(); - if (cookies != null) - { - for (Cookie cookie : cookies) - { - if (HttpUtil.COOKIE_BEARER_AUTHENTICATION.equals(cookie.getName())) - { - String sessionId = HttpUtil.getHeader(request, HttpUtil.HEADER_SCM_SESSION, null); - token = BearerToken.create(sessionId, cookie.getValue()); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (HttpUtil.COOKIE_BEARER_AUTHENTICATION.equals(cookie.getName())) { + token = BearerToken.create(SessionId.from(request).orElse(null), cookie.getValue()); break; } diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index 897b251cfa..6213c234bb 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -31,7 +31,6 @@ package sonia.scm.security; -import com.google.common.collect.ImmutableSet; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.junit.jupiter.api.BeforeEach; @@ -42,7 +41,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; -import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -84,7 +82,7 @@ class BearerRealmTest { @Test void shouldDoGetAuthentication() { - BearerToken bearerToken = BearerToken.create("__session__", "__bearer__"); + BearerToken bearerToken = BearerToken.create(SessionId.valueOf("__session__"), "__bearer__"); AccessToken accessToken = mock(AccessToken.class); when(accessToken.getSubject()).thenReturn("trillian"); diff --git a/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java index 1c054111b7..d77d2068b9 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java @@ -31,20 +31,19 @@ package sonia.scm.web; -import javax.servlet.http.HttpServletRequest; import org.apache.shiro.authc.AuthenticationToken; - - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.security.BearerToken; import sonia.scm.security.SessionId; -import sonia.scm.util.HttpUtil; + +import javax.servlet.http.HttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; /** * @@ -90,7 +89,7 @@ class BearerWebTokenGeneratorTest { @Test void shouldCreateTokenWithSessionId(){ doReturn("Bearer asd").when(request).getHeader("Authorization"); - doReturn("bcd123").when(request).getHeader(HttpUtil.HEADER_SCM_SESSION); + doReturn("bcd123").when(request).getHeader(SessionId.PARAMETER); AuthenticationToken token = tokenGenerator.createToken(request); assertThat(token) diff --git a/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java index 45d2c46b83..8cfe8c1b31 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java @@ -76,7 +76,7 @@ class CookieBearerWebTokenGeneratorTest { @Test void shouldCreateTokenWithSessionId() { - when(request.getHeader(HttpUtil.HEADER_SCM_SESSION)).thenReturn("abc123"); + when(request.getHeader(SessionId.PARAMETER)).thenReturn("abc123"); assignBearerCookie("authc"); diff --git a/yarn.lock b/yarn.lock index 42d56ee047..a2d1ea2670 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6580,11 +6580,6 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -event-source-polyfill@^1.0.9: - version "1.0.12" - resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.12.tgz#38546c4fee76dcadae2560185610ae46c5a39520" - integrity sha512-WjOTn0LIbaN08z/8gNt3GYAomAdm6cZ2lr/QdvhTTEipr5KR6lds2ziUH+p/Iob4Lk6NClKhwPOmn1NjQEcJCg== - eventemitter3@^3.1.0: version "3.1.2" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"