mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-06 01:48:43 +02:00
Merge pull request #1065 from scm-manager/feature/use_buildin_eventsource
Feature use built-in eventsource
This commit is contained in:
@@ -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
13
pom.xml
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 --------------------------------------------------------------
|
||||
|
||||
42
scm-core/src/test/java/sonia/scm/security/SessionIdTest.java
Normal file
42
scm-core/src/test/java/sonia/scm/security/SessionIdTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user