Merge pull request #1065 from scm-manager/feature/use_buildin_eventsource

Feature use built-in eventsource
This commit is contained in:
René Pfeuffer
2020-03-23 09:59:13 +01:00
committed by GitHub
15 changed files with 174 additions and 71 deletions

View File

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

13
pom.xml
View File

@@ -254,6 +254,17 @@
<version>${resteasy.version}</version>
</dependency>
<!--
Ensure smallrye-config is at least 1.6.2,
smallrye-config is a transitive dependency of resteasy-core.
https://snyk.io/vuln/SNYK-JAVA-IOSMALLRYECONFIG-548898
-->
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
@@ -837,7 +848,7 @@
<servlet.version>3.0.1</servlet.version>
<jaxrs.version>2.1.1</jaxrs.version>
<resteasy.version>4.4.2.Final</resteasy.version>
<resteasy.version>4.5.2.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version>
<jackson.version>2.10.2</jackson.version>
<guice.version>4.2.2</guice.version>

View File

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

View File

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

View File

@@ -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<String> 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<String> client = getHeaderOrGetParameter(request, HEADER_SCM_CLIENT);
return client.isPresent() && SCM_CLIENT_WUI.equalsIgnoreCase(client.get());
}
//~--- methods --------------------------------------------------------------

View File

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

View File

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

View File

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

View File

@@ -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<Response> {
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]);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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