mirror of
https://github.com/signalapp/Signal-Server
synced 2026-05-23 05:48:54 +01:00
Update to dropwizard 5.0.1
This commit is contained in:
committed by
ravi-signal
parent
0beeb8a935
commit
4c4282162f
@@ -49,7 +49,7 @@
|
||||
<braintree.version>3.48.0</braintree.version>
|
||||
<commons-csv.version>1.14.1</commons-csv.version>
|
||||
<commons-io.version>2.21.0</commons-io.version>
|
||||
<dropwizard.version>4.0.16</dropwizard.version>
|
||||
<dropwizard.version>5.0.1</dropwizard.version>
|
||||
<!-- Note: when updating FoundationDB, also include a copy of `libfdb_c.so` from the FoundationDB release at
|
||||
src/main/jib/usr/lib/libfdb_c.so. We use x86_64 builds without AVX instructions enabled (i.e. FoundationDB versions
|
||||
with even-numbered patch versions). Also when updating FoundationDB, make sure to update the version of FoundationDB
|
||||
@@ -253,12 +253,6 @@
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.3.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>9.9.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
@@ -361,7 +355,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.wiremock</groupId>
|
||||
<artifactId>wiremock</artifactId>
|
||||
<artifactId>wiremock-jetty12</artifactId>
|
||||
<version>3.13.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
+6
-6
@@ -23,7 +23,7 @@
|
||||
<opentelemetry-logback-appender-1.0.version>2.22.0-alpha</opentelemetry-logback-appender-1.0.version>
|
||||
<storekit.version>4.0.0</storekit.version>
|
||||
<webauthn4j.version>0.30.2.RELEASE</webauthn4j.version>
|
||||
<jetty.http2-client.version>11.0.26</jetty.http2-client.version>
|
||||
<jetty.http2-client.version>12.1.5</jetty.http2-client.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -140,7 +140,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.http2</groupId>
|
||||
<artifactId>http2-http-client-transport</artifactId>
|
||||
<artifactId>jetty-http2-client-transport</artifactId>
|
||||
<scope>test</scope>
|
||||
<version>${jetty.http2-client.version}</version>
|
||||
</dependency>
|
||||
@@ -249,15 +249,15 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-jetty-api</artifactId>
|
||||
<artifactId>jetty-websocket-jetty-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
<groupId>org.eclipse.jetty.ee10</groupId>
|
||||
<artifactId>jetty-ee10-servlets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-jetty-client</artifactId>
|
||||
<artifactId>jetty-websocket-jetty-client</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ import io.netty.resolver.dns.DnsNameResolver;
|
||||
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
||||
import io.netty.util.Mapping;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.ServletRegistration;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
@@ -52,7 +50,6 @@ import java.net.http.HttpClient;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
@@ -67,9 +64,9 @@ import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry;
|
||||
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
import org.signal.i18n.HeaderControlledResourceBundleLookup;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
@@ -142,10 +139,12 @@ import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.filters.ExternalRequestFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.PriorityFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RestDeprecationFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.StripContentLengthOnConnectFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
|
||||
import org.whispersystems.textsecuregcm.grpc.AccountsAnonymousGrpcService;
|
||||
import org.whispersystems.textsecuregcm.grpc.AccountsGrpcService;
|
||||
@@ -203,7 +202,7 @@ import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
|
||||
import org.whispersystems.textsecuregcm.metrics.CallQualitySurveyManager;
|
||||
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsHttpChannelListener;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsHttpEventHandler;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.MicrometerAwsSdkMetricPublisher;
|
||||
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
|
||||
@@ -628,7 +627,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
ScheduledExecutorService cloudflareTurnRetryExecutor = ScheduledExecutorServiceBuilder.of(environment, "cloudflareTurnRetry").threads(1).build();
|
||||
ScheduledExecutorService messagePollExecutor = ScheduledExecutorServiceBuilder.of(environment, "messagePollExecutor").threads(1).build();
|
||||
ScheduledExecutorService provisioningWebsocketTimeoutExecutor = ScheduledExecutorServiceBuilder.of(environment, "provisioningWebsocketTimeout").threads(1).build();
|
||||
ScheduledExecutorService jmxDumper = ScheduledExecutorServiceBuilder.of(environment, "jmxDumper").threads(1).build();
|
||||
|
||||
final ManagedEventLoopGroup<NioEventLoopGroup> dnsResolutionEventLoopGroup = new ManagedEventLoopGroup<>(new NioEventLoopGroup());
|
||||
final DnsNameResolver cloudflareDnsResolver = new DnsNameResolverBuilder(dnsResolutionEventLoopGroup.getEventLoopGroup().next())
|
||||
@@ -1057,17 +1055,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.lifecycle().manage(localGrpcServer);
|
||||
environment.lifecycle().manage(omnibusH2Server);
|
||||
|
||||
final List<Filter> filters = new ArrayList<>();
|
||||
filters.add(remoteDeprecationFilter);
|
||||
filters.add(new RemoteAddressFilter());
|
||||
filters.add(new TimestampResponseFilter());
|
||||
|
||||
for (Filter filter : filters) {
|
||||
environment.servlets()
|
||||
.addFilter(filter.getClass().getSimpleName(), filter)
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
}
|
||||
|
||||
if (!config.getExternalRequestFilterConfiguration().paths().isEmpty()) {
|
||||
environment.servlets().addFilter(ExternalRequestFilter.class.getSimpleName(),
|
||||
new ExternalRequestFilter(config.getExternalRequestFilterConfiguration().permittedInternalRanges(),
|
||||
@@ -1084,9 +1071,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
final String websocketServletPath = "/v1/websocket/";
|
||||
final String provisioningWebsocketServletPath = "/v1/websocket/provisioning/";
|
||||
|
||||
final MetricsHttpChannelListener metricsHttpChannelListener = new MetricsHttpChannelListener(clientReleaseManager,
|
||||
Set.of(websocketServletPath, provisioningWebsocketServletPath, "/health-check"));
|
||||
metricsHttpChannelListener.configure(environment);
|
||||
MetricsHttpEventHandler.configure(environment, Metrics.globalRegistry, clientReleaseManager, Set.of(websocketServletPath, provisioningWebsocketServletPath, "/health-check"));
|
||||
final MessageMetrics messageMetrics = new MessageMetrics();
|
||||
|
||||
// BufferingInterceptor is needed on the base environment but not the WebSocketEnvironment,
|
||||
@@ -1210,36 +1195,36 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
provisioningEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (context, container) -> {
|
||||
final WebSocketExtensionRegistry extensionRegistry = WebSocketServerComponents
|
||||
.getWebSocketComponents(environment.getApplicationContext().getServletContext())
|
||||
.getExtensionRegistry();
|
||||
if (config.getWebSocketConfiguration().isDisablePerMessageDeflate()) {
|
||||
extensionRegistry.unregister("permessage-deflate");
|
||||
} else if (config.getWebSocketConfiguration().isDisableCrossMessageOutgoingCompression()) {
|
||||
extensionRegistry.unregister("permessage-deflate");
|
||||
extensionRegistry.register("permessage-deflate", NoContextTakeoverPerMessageDeflateExtension.class);
|
||||
}
|
||||
});
|
||||
|
||||
WebSocketResourceProviderFactory<AuthenticatedDevice> webSocketServlet = new WebSocketResourceProviderFactory<>(
|
||||
webSocketEnvironment, AuthenticatedDevice.class, config.getWebSocketConfiguration(),
|
||||
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
webSocketEnvironment, AuthenticatedDevice.class, RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
WebSocketResourceProviderFactory<AuthenticatedDevice> provisioningServlet = new WebSocketResourceProviderFactory<>(
|
||||
provisioningEnvironment, AuthenticatedDevice.class, config.getWebSocketConfiguration(),
|
||||
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
provisioningEnvironment, AuthenticatedDevice.class, RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet);
|
||||
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(),
|
||||
(servletContext, container) -> {
|
||||
container.addMapping(websocketServletPath, webSocketServlet);
|
||||
container.addMapping(provisioningWebsocketServletPath, provisioningServlet);
|
||||
|
||||
websocket.addMapping(websocketServletPath);
|
||||
websocket.setAsyncSupported(true);
|
||||
PriorityFilter.ensureFilter(servletContext, new StripContentLengthOnConnectFilter());
|
||||
PriorityFilter.ensureFilter(servletContext, new TimestampResponseFilter());
|
||||
PriorityFilter.ensureFilter(servletContext, new RemoteAddressFilter());
|
||||
PriorityFilter.ensureFilter(servletContext, remoteDeprecationFilter);
|
||||
|
||||
provisioning.addMapping(provisioningWebsocketServletPath);
|
||||
provisioning.setAsyncSupported(true);
|
||||
container.setMaxBinaryMessageSize(config.getWebSocketConfiguration().getMaxBinaryMessageSize());
|
||||
container.setMaxTextMessageSize(config.getWebSocketConfiguration().getMaxTextMessageSize());
|
||||
|
||||
final WebSocketExtensionRegistry extensionRegistry = WebSocketServerComponents
|
||||
.getWebSocketComponents(environment.getApplicationContext())
|
||||
.getExtensionRegistry();
|
||||
if (config.getWebSocketConfiguration().isDisablePerMessageDeflate()) {
|
||||
extensionRegistry.unregister("permessage-deflate");
|
||||
} else if (config.getWebSocketConfiguration().isDisableCrossMessageOutgoingCompression()) {
|
||||
extensionRegistry.unregister("permessage-deflate");
|
||||
extensionRegistry.register("permessage-deflate", NoContextTakeoverPerMessageDeflateExtension.class);
|
||||
}
|
||||
});
|
||||
|
||||
environment.admin().addTask(new SetRequestLoggingEnabledTask());
|
||||
|
||||
}
|
||||
|
||||
private void registerExceptionMappers(Environment environment,
|
||||
|
||||
+2
-3
@@ -10,10 +10,9 @@ import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.websocket.auth.AuthenticatedWebSocketUpgradeFilter;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.filters;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import org.eclipse.jetty.ee10.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.ee10.servlet.FilterMapping;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
|
||||
public class PriorityFilter {
|
||||
|
||||
private PriorityFilter() {}
|
||||
|
||||
private static FilterHolder getFilter(ServletContext servletContext, final Class<? extends Filter> filterClass) {
|
||||
final ContextHandler contextHandler = Objects.requireNonNull(ServletContextHandler.getServletContextHandler(servletContext));
|
||||
final ServletHandler servletHandler = contextHandler.getDescendant(ServletHandler.class);
|
||||
return servletHandler.getFilter(filterClass.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a filter is available on the provided ServletContext, a new filter will added if one does not already
|
||||
* exist.
|
||||
* <p>
|
||||
* If a new filter is added, it will be added before all other filters.
|
||||
* <p>
|
||||
* Modeled after {@link org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter#ensureFilter(ServletContext)},
|
||||
* since its use of {@link org.eclipse.jetty.ee10.servlet.ServletHandler#prependFilter(FilterHolder)} is what makes
|
||||
* this necessary.
|
||||
*/
|
||||
public static void ensureFilter(final ServletContext servletContext, final Filter filter) {
|
||||
FilterHolder existingFilter = getFilter(servletContext, filter.getClass());
|
||||
if (existingFilter != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext);
|
||||
final ServletHandler servletHandler = contextHandler.getDescendant(ServletHandler.class);
|
||||
|
||||
final String pathSpec = "/*";
|
||||
final FilterHolder holder = new FilterHolder(filter);
|
||||
holder.setName(filter.getClass().getName());
|
||||
holder.setAsyncSupported(true);
|
||||
|
||||
final FilterMapping mapping = new FilterMapping();
|
||||
mapping.setFilterName(holder.getName());
|
||||
mapping.setPathSpec(pathSpec);
|
||||
mapping.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
|
||||
|
||||
// Add as the first filter in the list.
|
||||
servletHandler.prependFilter(holder);
|
||||
servletHandler.prependFilterMapping(mapping);
|
||||
|
||||
// If we create the filter we must also make sure it is removed if the context is stopped.
|
||||
contextHandler.addEventListener(new LifeCycle.Listener()
|
||||
{
|
||||
@Override
|
||||
public void lifeCycleStopping(LifeCycle event)
|
||||
{
|
||||
servletHandler.removeFilterHolder(holder);
|
||||
servletHandler.removeFilterMapping(mapping);
|
||||
contextHandler.removeEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%sCleanupListener", filter.getClass().getSimpleName());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
-4
@@ -26,10 +26,6 @@ public class RemoteAddressFilter implements Filter {
|
||||
public static final String REMOTE_ADDRESS_ATTRIBUTE_NAME = RemoteAddressFilter.class.getName() + ".remoteAddress";
|
||||
private static final Logger logger = LoggerFactory.getLogger(RemoteAddressFilter.class);
|
||||
|
||||
|
||||
public RemoteAddressFilter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.filters;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.server.HttpStream;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
||||
/// Our current version of jetty (12.1.5) has a bug where it includes content-length:0 on
|
||||
/// CONNECT websocket upgrade requests. Providing an HTTP/2 header frame with a
|
||||
/// content-length that does not match the sum of the lengths of the data frames is technically
|
||||
/// a malformed HTTP/2 stream and our netty-based reverse proxy implementation rejects it. This
|
||||
/// filter strips out the superfluous content-length at stream-send time. It can be removed once
|
||||
/// we update to a jetty version that fixes [jetty/jetty.project#15074](https://github.com/jetty/jetty.project/issues/15074)
|
||||
public class StripContentLengthOnConnectFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
if (request instanceof HttpServletRequest hsr &&
|
||||
HttpVersion.HTTP_2.is(hsr.getProtocol()) &&
|
||||
HttpMethod.CONNECT.is(hsr.getMethod())) {
|
||||
final Request coreRequest = ServletContextRequest.getServletContextRequest(hsr);
|
||||
if (coreRequest != null) {
|
||||
coreRequest.addHttpStreamWrapper(StripContentLengthStream::new);
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private static class StripContentLengthStream extends HttpStream.Wrapper {
|
||||
|
||||
StripContentLengthStream(final HttpStream wrapped) {
|
||||
super(wrapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer content,
|
||||
Callback callback) {
|
||||
if (response != null && response.getStatus() == 200 && response.getHttpFields()
|
||||
.contains(HttpHeader.CONTENT_LENGTH)) {
|
||||
final HttpFields fieldsWithoutContentLengthHeader =
|
||||
HttpFields.build(response.getHttpFields()).remove(HttpHeader.CONTENT_LENGTH);
|
||||
response = new MetaData.Response(
|
||||
response.getStatus(),
|
||||
response.getReason(),
|
||||
response.getHttpVersion(),
|
||||
fieldsWithoutContentLengthHeader,
|
||||
-1,
|
||||
response.getTrailersSupplier());
|
||||
}
|
||||
super.send(request, response, last, content, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -14,7 +14,7 @@ import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
/**
|
||||
* Delegates request events to a listener that captures and reports request-level metrics.
|
||||
*
|
||||
* @see MetricsHttpChannelListener
|
||||
* @see MetricsHttpEventHandler
|
||||
* @see MetricsRequestEventListener
|
||||
*/
|
||||
public class MetricsApplicationEventListener implements ApplicationEventListener {
|
||||
@@ -23,7 +23,7 @@ public class MetricsApplicationEventListener implements ApplicationEventListener
|
||||
|
||||
public MetricsApplicationEventListener(final TrafficSource trafficSource, final ClientReleaseManager clientReleaseManager) {
|
||||
if (trafficSource == TrafficSource.HTTP) {
|
||||
throw new IllegalArgumentException("Use " + MetricsHttpChannelListener.class.getName() + " for HTTP traffic");
|
||||
throw new IllegalArgumentException("Use " + MetricsHttpEventHandler.class.getName() + " for HTTP traffic");
|
||||
}
|
||||
this.metricsRequestEventListener = new MetricsRequestEventListener(trafficSource, clientReleaseManager);
|
||||
}
|
||||
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseFilter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.util.component.Container;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.glassfish.jersey.server.ExtendedUriInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
import org.whispersystems.textsecuregcm.util.logging.UriInfoUtil;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
/**
|
||||
* Gathers and reports HTTP request metrics at the Jetty container level, which sits above Jersey. In order to get
|
||||
* templated Jersey request paths, it implements {@link jakarta.ws.rs.container.ContainerResponseFilter}, in order to give
|
||||
* itself access to the template. It is limited to {@link TrafficSource#HTTP} requests.
|
||||
* <p>
|
||||
* It implements {@link LifeCycle.Listener} without overriding methods, so that it can be an event listener that
|
||||
* Dropwizard will attach to the container—the {@link Container.Listener} implementation is where it attaches
|
||||
* itself to any {@link Connector}s.
|
||||
*
|
||||
* @see MetricsRequestEventListener
|
||||
*/
|
||||
public class MetricsHttpChannelListener implements HttpChannel.Listener, Container.Listener, LifeCycle.Listener,
|
||||
ContainerResponseFilter {
|
||||
|
||||
private static final Set<String> EXPECTED_HTTP_METHODS =
|
||||
Set.of("GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH");
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MetricsHttpChannelListener.class);
|
||||
|
||||
private record RequestInfo(String path, String method, int statusCode, @Nullable String userAgent) {
|
||||
}
|
||||
|
||||
private final ClientReleaseManager clientReleaseManager;
|
||||
private final Set<String> servletPaths;
|
||||
|
||||
// Use the same counter namespace as MetricsRequestEventListener for continuity
|
||||
public static final String REQUEST_COUNTER_NAME = MetricsRequestEventListener.REQUEST_COUNTER_NAME;
|
||||
public static final String REQUESTS_BY_VERSION_COUNTER_NAME = MetricsRequestEventListener.REQUESTS_BY_VERSION_COUNTER_NAME;
|
||||
@VisibleForTesting
|
||||
static final String RESPONSE_BYTES_COUNTER_NAME = MetricsRequestEventListener.RESPONSE_BYTES_COUNTER_NAME;
|
||||
@VisibleForTesting
|
||||
static final String REQUEST_BYTES_COUNTER_NAME = MetricsRequestEventListener.REQUEST_BYTES_COUNTER_NAME;
|
||||
@VisibleForTesting
|
||||
static final String URI_INFO_PROPERTY_NAME = MetricsHttpChannelListener.class.getName() + ".uriInfo";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String PATH_TAG = "path";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String METHOD_TAG = "method";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String STATUS_CODE_TAG = "status";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String TRAFFIC_SOURCE_TAG = "trafficSource";
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
|
||||
public MetricsHttpChannelListener(final ClientReleaseManager clientReleaseManager, final Set<String> servletPaths) {
|
||||
this(Metrics.globalRegistry, clientReleaseManager, servletPaths);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
MetricsHttpChannelListener(final MeterRegistry meterRegistry, final ClientReleaseManager clientReleaseManager,
|
||||
final Set<String> servletPaths) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.clientReleaseManager = clientReleaseManager;
|
||||
this.servletPaths = servletPaths;
|
||||
}
|
||||
|
||||
public void configure(final Environment environment) {
|
||||
// register as ContainerResponseFilter
|
||||
environment.jersey().register(this);
|
||||
|
||||
// hook into lifecycle events, to react to the Connector being added
|
||||
environment.lifecycle().addEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestFailure(final Request request, final Throwable failure) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
final RequestInfo requestInfo = getRequestInfo(request);
|
||||
|
||||
logger.debug("Request failure: {} {} ({}) [{}] ",
|
||||
requestInfo.method(),
|
||||
requestInfo.path(),
|
||||
requestInfo.userAgent(),
|
||||
requestInfo.statusCode(), failure);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponseFailure(Request request, Throwable failure) {
|
||||
|
||||
if (failure instanceof org.eclipse.jetty.io.EofException) {
|
||||
// the client disconnected early
|
||||
return;
|
||||
}
|
||||
|
||||
final RequestInfo requestInfo = getRequestInfo(request);
|
||||
|
||||
logger.warn("Response failure: {} {} ({}) [{}] ",
|
||||
requestInfo.method(),
|
||||
requestInfo.path(),
|
||||
requestInfo.userAgent(),
|
||||
requestInfo.statusCode(), failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(final Request request) {
|
||||
|
||||
final RequestInfo requestInfo = getRequestInfo(request);
|
||||
|
||||
@Nullable final UserAgent userAgent;
|
||||
{
|
||||
UserAgent parsedUserAgent;
|
||||
|
||||
try {
|
||||
parsedUserAgent = UserAgentUtil.parseUserAgentString(requestInfo.userAgent());
|
||||
} catch (final UnrecognizedUserAgentException e) {
|
||||
parsedUserAgent = null;
|
||||
}
|
||||
|
||||
userAgent = parsedUserAgent;
|
||||
}
|
||||
|
||||
final Tag platformTag = UserAgentTagUtil.getPlatformTag(userAgent);
|
||||
|
||||
final List<Tag> tags = new ArrayList<>(5);
|
||||
tags.add(Tag.of(PATH_TAG, requestInfo.path()));
|
||||
tags.add(Tag.of(METHOD_TAG, requestInfo.method()));
|
||||
tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(requestInfo.statusCode())));
|
||||
tags.add(Tag.of(TRAFFIC_SOURCE_TAG, TrafficSource.HTTP.name().toLowerCase()));
|
||||
tags.add(platformTag);
|
||||
|
||||
final Optional<Tag> maybeClientVersionTag =
|
||||
UserAgentTagUtil.getClientVersionTag(userAgent, clientReleaseManager);
|
||||
|
||||
maybeClientVersionTag.ifPresent(tags::add);
|
||||
|
||||
meterRegistry.counter(REQUEST_COUNTER_NAME, tags).increment();
|
||||
|
||||
meterRegistry.counter(RESPONSE_BYTES_COUNTER_NAME, tags).increment(request.getResponse().getContentCount());
|
||||
meterRegistry.counter(REQUEST_BYTES_COUNTER_NAME, tags).increment(request.getContentRead());
|
||||
|
||||
maybeClientVersionTag.ifPresent(clientVersionTag -> meterRegistry.counter(REQUESTS_BY_VERSION_COUNTER_NAME,
|
||||
Tags.of(clientVersionTag, platformTag))
|
||||
.increment());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beanAdded(final Container parent, final Object child) {
|
||||
if (child instanceof Connector connector) {
|
||||
connector.addBean(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beanRemoved(final Container parent, final Object child) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext)
|
||||
throws IOException {
|
||||
requestContext.setProperty(URI_INFO_PROPERTY_NAME, requestContext.getUriInfo());
|
||||
}
|
||||
|
||||
private RequestInfo getRequestInfo(Request request) {
|
||||
final String path = Optional.ofNullable(request.getAttribute(URI_INFO_PROPERTY_NAME))
|
||||
.map(attr -> UriInfoUtil.getPathTemplate((ExtendedUriInfo) attr))
|
||||
.orElseGet(() ->
|
||||
Optional.ofNullable(request.getPathInfo())
|
||||
.filter(servletPaths::contains)
|
||||
.orElse("unknown")
|
||||
);
|
||||
|
||||
// Response cannot be null, but its status might not always reflect an actual response status, since it gets
|
||||
// initialized to 200
|
||||
final int status = request.getResponse().getStatus();
|
||||
|
||||
@Nullable final String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
|
||||
|
||||
return new RequestInfo(path, normalizeMethod(request.getMethod()), status, userAgent);
|
||||
}
|
||||
|
||||
static String normalizeMethod(@Nullable final String method) {
|
||||
if (StringUtils.isBlank(method)) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return EXPECTED_HTTP_METHODS.contains(method.toUpperCase(Locale.ROOT)) ? method : "unknown";
|
||||
}
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.metrics;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseContext;
|
||||
import jakarta.ws.rs.container.ContainerResponseFilter;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.EventsHandler;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.glassfish.jersey.server.ExtendedUriInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
import org.whispersystems.textsecuregcm.util.logging.UriInfoUtil;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
/**
|
||||
* Gathers and reports HTTP request metrics at the Jetty container level, which sits above Jersey. In order to get
|
||||
* templated Jersey request paths, it adds a {@link jakarta.ws.rs.container.ContainerResponseFilter}, in order to give
|
||||
* itself access to the template. It is limited to {@link TrafficSource#HTTP} requests.
|
||||
*
|
||||
* @see MetricsRequestEventListener
|
||||
*/
|
||||
public class MetricsHttpEventHandler extends EventsHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MetricsHttpEventHandler.class);
|
||||
|
||||
private static final Set<String> EXPECTED_HTTP_METHODS =
|
||||
Set.of("GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH");
|
||||
|
||||
private final ClientReleaseManager clientReleaseManager;
|
||||
private final Set<String> servletPaths;
|
||||
|
||||
// Use the same counter namespace as MetricsRequestEventListener for continuity
|
||||
public static final String REQUEST_COUNTER_NAME = MetricsRequestEventListener.REQUEST_COUNTER_NAME;
|
||||
public static final String REQUESTS_BY_VERSION_COUNTER_NAME = MetricsRequestEventListener.REQUESTS_BY_VERSION_COUNTER_NAME;
|
||||
@VisibleForTesting
|
||||
static final String RESPONSE_BYTES_COUNTER_NAME = MetricsRequestEventListener.RESPONSE_BYTES_COUNTER_NAME;
|
||||
@VisibleForTesting
|
||||
static final String REQUEST_BYTES_COUNTER_NAME = MetricsRequestEventListener.REQUEST_BYTES_COUNTER_NAME;
|
||||
@VisibleForTesting
|
||||
static final String REQUEST_INFO_PROPERTY_NAME = MetricsHttpEventHandler.class.getName() + ".requestInfo";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String PATH_TAG = "path";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String METHOD_TAG = "method";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String STATUS_CODE_TAG = "status";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String TRAFFIC_SOURCE_TAG = "trafficSource";
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
@VisibleForTesting
|
||||
MetricsHttpEventHandler(
|
||||
final Handler handler,
|
||||
final MeterRegistry meterRegistry,
|
||||
final ClientReleaseManager clientReleaseManager,
|
||||
final Set<String> servletPaths) {
|
||||
super(handler);
|
||||
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.clientReleaseManager = clientReleaseManager;
|
||||
this.servletPaths = servletPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link MetricsHttpEventHandler}
|
||||
*
|
||||
* @param environment A dropwizard {@link org.eclipse.jetty.util.component.Environment}
|
||||
* @param meterRegistry The meter registry to register metrics with
|
||||
* @param clientReleaseManager A {@link ClientReleaseManager} that determines what tags to include with metrics
|
||||
* @param servletPaths An allow-list of paths to include in metric tags for requests that are handled by above
|
||||
* Jersey
|
||||
*/
|
||||
public static void configure(final Environment environment, final MeterRegistry meterRegistry,
|
||||
final ClientReleaseManager clientReleaseManager, final Set<String> servletPaths) {
|
||||
// register a filter that will set the initial request info
|
||||
environment.jersey().register(new SetInfoRequestFilter());
|
||||
|
||||
// hook into lifecycle events, to react to the Connector being added
|
||||
environment.lifecycle().addEventListener(new LifeCycle.Listener() {
|
||||
@Override
|
||||
public void lifeCycleStarting(LifeCycle event) {
|
||||
if (event instanceof Server server) {
|
||||
server.setHandler(
|
||||
new MetricsHttpEventHandler(server.getHandler(), meterRegistry, clientReleaseManager, servletPaths));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onResponseFailure(Request request, int status, Throwable failure) {
|
||||
|
||||
if (failure instanceof org.eclipse.jetty.io.EofException) {
|
||||
// the client disconnected early
|
||||
return;
|
||||
}
|
||||
|
||||
final RequestInfo requestInfo = getRequestInfo(request);
|
||||
|
||||
logger.warn("Response failure: {} {} ({}) [{}] ",
|
||||
requestInfo.method,
|
||||
requestInfo.path,
|
||||
requestInfo.userAgent,
|
||||
status,
|
||||
failure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(Request request, int status, HttpFields headers, Throwable failure) {
|
||||
|
||||
super.onComplete(request, status, headers, failure);
|
||||
|
||||
if (failure != null) {
|
||||
onResponseFailure(request, status, failure);
|
||||
}
|
||||
|
||||
final RequestInfo requestInfo = getRequestInfo(request);
|
||||
|
||||
@Nullable final UserAgent userAgent;
|
||||
{
|
||||
UserAgent parsedUserAgent;
|
||||
|
||||
try {
|
||||
parsedUserAgent = UserAgentUtil.parseUserAgentString(requestInfo.userAgent);
|
||||
} catch (final UnrecognizedUserAgentException e) {
|
||||
parsedUserAgent = null;
|
||||
}
|
||||
|
||||
userAgent = parsedUserAgent;
|
||||
}
|
||||
final Tag platformTag = UserAgentTagUtil.getPlatformTag(userAgent);
|
||||
|
||||
|
||||
final List<Tag> tags = new ArrayList<>(5);
|
||||
tags.add(Tag.of(PATH_TAG, requestInfo.path));
|
||||
tags.add(Tag.of(METHOD_TAG, requestInfo.method));
|
||||
tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(status)));
|
||||
tags.add(Tag.of(TRAFFIC_SOURCE_TAG, TrafficSource.HTTP.name().toLowerCase()));
|
||||
tags.add(platformTag);
|
||||
|
||||
final Optional<Tag> maybeClientVersionTag =
|
||||
UserAgentTagUtil.getClientVersionTag(userAgent, clientReleaseManager);
|
||||
|
||||
maybeClientVersionTag.ifPresent(tags::add);
|
||||
|
||||
meterRegistry.counter(REQUEST_COUNTER_NAME, tags).increment();
|
||||
meterRegistry.counter(RESPONSE_BYTES_COUNTER_NAME, tags).increment(requestInfo.responseBytes);
|
||||
meterRegistry.counter(REQUEST_BYTES_COUNTER_NAME, tags).increment(requestInfo.requestBytes);
|
||||
|
||||
maybeClientVersionTag.ifPresent(clientVersionTag -> meterRegistry.counter(REQUESTS_BY_VERSION_COUNTER_NAME,
|
||||
Tags.of(clientVersionTag, platformTag))
|
||||
.increment());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequestRead(final Request request, final Content.Chunk chunk) {
|
||||
super.onRequestRead(request, chunk);
|
||||
if (chunk != null) {
|
||||
getRequestInfo(request).requestBytes += chunk.remaining();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResponseWrite(final Request request, final boolean last, final ByteBuffer content) {
|
||||
super.onResponseWrite(request, last, content);
|
||||
if (content != null) {
|
||||
getRequestInfo(request).responseBytes += content.remaining();
|
||||
}
|
||||
}
|
||||
|
||||
private RequestInfo getRequestInfo(Request request) {
|
||||
Object obj = request.getAttribute(REQUEST_INFO_PROPERTY_NAME);
|
||||
if (obj != null && obj instanceof RequestInfo requestInfo) {
|
||||
return requestInfo;
|
||||
}
|
||||
|
||||
// Our ContainerResponseFilter has not run yet. It should eventually run, and will override the path we set here.
|
||||
// It may not run if this is a websocket upgrade request, a request handled by jetty directly, or a higher priority
|
||||
// filter aborted the request by throwing an exception, in which case we'll use this path. To avoid giving every
|
||||
// incorrect path a unique tag we check against a configured list of paths that we know would skip the filter.
|
||||
final RequestInfo newInfo = new RequestInfo(
|
||||
Optional.ofNullable(request.getHttpURI().getPath()).filter(servletPaths::contains).orElse("unknown"),
|
||||
normalizeMethod(request.getMethod()),
|
||||
request.getHeaders().get(HttpHeaders.USER_AGENT));
|
||||
|
||||
request.setAttribute(REQUEST_INFO_PROPERTY_NAME, newInfo);
|
||||
return newInfo;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class RequestInfo {
|
||||
|
||||
private String path;
|
||||
private final String method;
|
||||
private final @Nullable String userAgent;
|
||||
|
||||
private long requestBytes;
|
||||
private long responseBytes;
|
||||
|
||||
RequestInfo(@NotNull String path, @NotNull String method, @Nullable String userAgent) {
|
||||
this.path = path;
|
||||
this.method = method;
|
||||
this.userAgent = userAgent;
|
||||
this.requestBytes = 0;
|
||||
this.responseBytes = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
RequestInfo that = (RequestInfo) o;
|
||||
return requestBytes == that.requestBytes && responseBytes == that.responseBytes && Objects.equals(path, that.path)
|
||||
&& Objects.equals(method, that.method) && Objects.equals(userAgent, that.userAgent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(path, method, userAgent, requestBytes, responseBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class SetInfoRequestFilter implements ContainerResponseFilter {
|
||||
|
||||
@Override
|
||||
public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext) {
|
||||
// Construct the templated URI path. If no matching path is found, this will be ""
|
||||
final String path = UriInfoUtil.getPathTemplate((ExtendedUriInfo) requestContext.getUriInfo());
|
||||
final Object obj = requestContext.getProperty(REQUEST_INFO_PROPERTY_NAME);
|
||||
if (obj != null && obj instanceof RequestInfo requestInfo) {
|
||||
requestInfo.path = path;
|
||||
} else {
|
||||
requestContext.setProperty(REQUEST_INFO_PROPERTY_NAME,
|
||||
new RequestInfo(path, requestContext.getMethod(), requestContext.getHeaderString(HttpHeaders.USER_AGENT)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String normalizeMethod(@Nullable final String method) {
|
||||
if (StringUtils.isBlank(method)) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return EXPECTED_HTTP_METHODS.contains(method.toUpperCase(Locale.ROOT)) ? method : "unknown";
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -31,7 +31,7 @@ import org.whispersystems.websocket.WebSocketResourceProvider;
|
||||
|
||||
/**
|
||||
* Gathers and reports request-level metrics for WebSocket traffic only.
|
||||
* For HTTP traffic, use {@link MetricsHttpChannelListener}.
|
||||
* For HTTP traffic, use {@link MetricsHttpEventHandler}.
|
||||
*/
|
||||
public class MetricsRequestEventListener implements RequestEventListener {
|
||||
|
||||
@@ -69,7 +69,7 @@ public class MetricsRequestEventListener implements RequestEventListener {
|
||||
this(trafficSource, Metrics.globalRegistry, clientReleaseManager);
|
||||
|
||||
if (trafficSource == TrafficSource.HTTP) {
|
||||
logger.warn("Use {} for HTTP traffic", MetricsHttpChannelListener.class.getName());
|
||||
logger.warn("Use {} for HTTP traffic", MetricsHttpEventHandler.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public class MetricsRequestEventListener implements RequestEventListener {
|
||||
|
||||
final List<Tag> tags = new ArrayList<>();
|
||||
tags.add(Tag.of(PATH_TAG, UriInfoUtil.getPathTemplate(event.getUriInfo())));
|
||||
tags.add(Tag.of(METHOD_TAG, MetricsHttpChannelListener.normalizeMethod(event.getContainerRequest().getMethod())));
|
||||
tags.add(Tag.of(METHOD_TAG, MetricsHttpEventHandler.normalizeMethod(event.getContainerRequest().getMethod())));
|
||||
tags.add(Tag.of(STATUS_CODE_TAG, String.valueOf(Optional
|
||||
.ofNullable(event.getContainerResponse())
|
||||
.map(ContainerResponse::getStatus)
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.eclipse.jetty.util.resource.PathResourceFactory;
|
||||
import org.eclipse.jetty.util.security.CertificateUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -37,7 +37,7 @@ public class TlsCertificateExpirationUtil {
|
||||
|
||||
final KeyStore keyStore;
|
||||
try {
|
||||
keyStore = CertificateUtils.getKeyStore(Resource.newResource(keyStorePath), keyStoreType, keyStoreProvider,
|
||||
keyStore = CertificateUtils.getKeyStore(new PathResourceFactory().newResource(keyStorePath), keyStoreType, keyStoreProvider,
|
||||
keyStorePassword);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import javax.management.MBeanServer;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
public class JmxDumper implements Managed {
|
||||
private static final Logger log = LoggerFactory.getLogger(JmxDumper.class);
|
||||
|
||||
|
||||
private final ScheduledExecutorService executor;
|
||||
|
||||
public JmxDumper(final ScheduledExecutorService executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
// executor.schedule()
|
||||
}
|
||||
|
||||
private void dump() {
|
||||
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
|
||||
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -8,14 +8,14 @@ package org.whispersystems.textsecuregcm.websocket;
|
||||
import static org.whispersystems.textsecuregcm.util.HeaderUtils.basicCredentialsFromAuthHeader;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import javax.annotation.Nullable;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.websocket.auth.InvalidCredentialsException;
|
||||
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<AuthenticatedDevice> {
|
||||
@@ -27,7 +27,7 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Aut
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthenticatedDevice> authenticate(final UpgradeRequest request)
|
||||
public Optional<AuthenticatedDevice> authenticate(final JettyServerUpgradeRequest request)
|
||||
throws InvalidCredentialsException {
|
||||
|
||||
@Nullable final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
-2
@@ -15,7 +15,6 @@ import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.glassfish.jersey.server.ManagedAsync;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -35,7 +34,6 @@ public class BufferingInterceptorIntegrationTest {
|
||||
environment.jersey().register(testController);
|
||||
environment.jersey().register(new BufferingInterceptor());
|
||||
environment.jersey().register(new VirtualExecutorServiceProvider("virtual-thread-", 10));
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-16
@@ -18,28 +18,29 @@ import io.dropwizard.core.Configuration;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.testing.junit5.DropwizardAppExtension;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletRegistration;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfoProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.filters.PriorityFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
|
||||
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
@@ -52,6 +53,7 @@ import org.whispersystems.websocket.messages.WebSocketMessage;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
||||
public class ProvisioningTimeoutIntegrationTest {
|
||||
|
||||
private static final DropwizardAppExtension<Configuration> DROPWIZARD_APP_EXTENSION =
|
||||
@@ -77,9 +79,9 @@ public class ProvisioningTimeoutIntegrationTest {
|
||||
CompletableFuture<String> provisioningAddressFuture = new CompletableFuture<>();
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(final byte[] payload, final int offset, final int length) {
|
||||
public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
|
||||
try {
|
||||
WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload, offset, length);
|
||||
WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload);
|
||||
if (Objects.requireNonNull(webSocketMessage.getType()) == WebSocketMessage.Type.REQUEST_MESSAGE
|
||||
&& webSocketMessage.getRequestMessage().getPath().equals("/v1/address")) {
|
||||
MessageProtos.ProvisioningAddress provisioningAddress =
|
||||
@@ -92,7 +94,7 @@ public class ProvisioningTimeoutIntegrationTest {
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
super.onWebSocketBinary(payload, offset, length);
|
||||
super.onWebSocketBinary(payload, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,21 +108,17 @@ public class ProvisioningTimeoutIntegrationTest {
|
||||
final WebSocketEnvironment<AuthenticatedDevice> webSocketEnvironment =
|
||||
new WebSocketEnvironment<>(environment, webSocketConfiguration);
|
||||
|
||||
environment.servlets()
|
||||
.addFilter("RemoteAddressFilter", new RemoteAddressFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
webSocketEnvironment.setConnectListener(
|
||||
new ProvisioningConnectListener(mock(ProvisioningManager.class), () -> mock(AsnInfoProvider.class), mock(ClientReleaseManager.class), scheduler, Duration.ofSeconds(5)));
|
||||
|
||||
final WebSocketResourceProviderFactory<AuthenticatedDevice> webSocketServlet =
|
||||
new WebSocketResourceProviderFactory<>(webSocketEnvironment, AuthenticatedDevice.class,
|
||||
webSocketConfiguration, REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
|
||||
final ServletRegistration.Dynamic websocketServlet = environment.servlets()
|
||||
.addServlet("WebSocket", webSocketServlet);
|
||||
websocketServlet.addMapping("/websocket");
|
||||
websocketServlet.setAsyncSupported(true);
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (servletContext, container) -> {
|
||||
container.addMapping("/websocket", webSocketServlet);
|
||||
PriorityFilter.ensureFilter(servletContext, new RemoteAddressFilter());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-14
@@ -9,8 +9,6 @@ import io.dropwizard.core.Configuration;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.testing.junit5.DropwizardAppExtension;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletRegistration;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
@@ -20,19 +18,20 @@ import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.glassfish.jersey.server.ManagedAsync;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.filters.PriorityFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
|
||||
import org.whispersystems.textsecuregcm.tests.util.TestWebsocketListener;
|
||||
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||
@@ -41,6 +40,7 @@ import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
||||
public class WebsocketResourceProviderIntegrationTest {
|
||||
private static final DropwizardAppExtension<Configuration> DROPWIZARD_APP_EXTENSION =
|
||||
new DropwizardAppExtension<>(TestApplication.class);
|
||||
@@ -72,9 +72,6 @@ public class WebsocketResourceProviderIntegrationTest {
|
||||
new WebSocketEnvironment<>(environment, webSocketConfiguration);
|
||||
|
||||
environment.jersey().register(testController);
|
||||
environment.servlets()
|
||||
.addFilter("RemoteAddressFilter", new RemoteAddressFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
webSocketEnvironment.jersey().register(testController);
|
||||
webSocketEnvironment.jersey().register(new RemoteAddressFilter());
|
||||
webSocketEnvironment.setAuthenticator(upgradeRequest -> Optional.of(mock(AuthenticatedDevice.class)));
|
||||
@@ -85,15 +82,13 @@ public class WebsocketResourceProviderIntegrationTest {
|
||||
|
||||
final WebSocketResourceProviderFactory<AuthenticatedDevice> webSocketServlet =
|
||||
new WebSocketResourceProviderFactory<>(webSocketEnvironment, AuthenticatedDevice.class,
|
||||
webSocketConfiguration, REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (servletContext, container) -> {
|
||||
container.addMapping("/websocket", webSocketServlet);
|
||||
PriorityFilter.ensureFilter(servletContext, new RemoteAddressFilter());
|
||||
});
|
||||
|
||||
final ServletRegistration.Dynamic websocketServlet =
|
||||
environment.servlets().addServlet("WebSocket", webSocketServlet);
|
||||
|
||||
websocketServlet.addMapping("/websocket");
|
||||
websocketServlet.setAsyncSupported(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
|
||||
import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2;
|
||||
import org.eclipse.jetty.io.ClientConnector;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
|
||||
+2
-6
@@ -210,8 +210,7 @@ class PhoneVerificationTokenManagerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void verifyRecoveryPasswordManagerException(final Throwable recoveryPasswordManagerException)
|
||||
throws ExecutionException, InterruptedException, TimeoutException {
|
||||
void verifyRecoveryPasswordManagerException(final Throwable recoveryPasswordManagerException) {
|
||||
|
||||
final ContainerRequestContext containerRequestContext = mock(ContainerRequestContext.class);
|
||||
final byte[] recoveryPassword = TestRandomUtil.nextBytes(16);
|
||||
@@ -219,11 +218,8 @@ class PhoneVerificationTokenManagerTest {
|
||||
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(containerRequestContext, PHONE_NUMBER))
|
||||
.thenReturn(true);
|
||||
|
||||
@SuppressWarnings("unchecked") final CompletableFuture<Boolean> mockFuture = mock(CompletableFuture.class);
|
||||
when(mockFuture.get(anyLong(), any())).thenThrow(recoveryPasswordManagerException);
|
||||
|
||||
when(registrationRecoveryPasswordsManager.verify(PHONE_NUMBER_IDENTIFIER, recoveryPassword))
|
||||
.thenReturn(mockFuture);
|
||||
.thenReturn(CompletableFuture.failedFuture(recoveryPasswordManagerException));
|
||||
|
||||
assertThrows(ServerErrorException.class, () -> phoneVerificationTokenManager.verify(containerRequestContext,
|
||||
PHONE_NUMBER,
|
||||
|
||||
+15
-24
@@ -13,20 +13,17 @@ import io.dropwizard.core.Configuration;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.testing.junit5.DropwizardAppExtension;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.client.Client;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -35,14 +32,15 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.security.auth.Subject;
|
||||
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.eclipse.jetty.util.HostPort;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
@@ -55,6 +53,7 @@ import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFa
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
||||
class RemoteAddressFilterIntegrationTest {
|
||||
|
||||
private static final String WEBSOCKET_PREFIX = "/websocket";
|
||||
@@ -131,7 +130,7 @@ class RemoteAddressFilterIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClientEndpoint implements WebSocketListener {
|
||||
public static class ClientEndpoint implements Session.Listener.AutoDemanding {
|
||||
|
||||
private final String requestPath;
|
||||
private final CompletableFuture<byte[]> responseFuture;
|
||||
@@ -145,22 +144,19 @@ class RemoteAddressFilterIntegrationTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(final Session session) {
|
||||
public void onWebSocketOpen(final Session session) {
|
||||
final byte[] requestBytes = messageFactory.createRequest(Optional.of(1L), "GET", requestPath,
|
||||
List.of("Accept: application/json"),
|
||||
Optional.empty()).toByteArray();
|
||||
try {
|
||||
session.getRemote().sendBytes(ByteBuffer.wrap(requestBytes));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
session.sendBinary(ByteBuffer.wrap(requestBytes), Callback.NOOP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(final byte[] payload, final int offset, final int length) {
|
||||
public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
|
||||
|
||||
try {
|
||||
WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload, offset, length);
|
||||
WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload);
|
||||
|
||||
if (Objects.requireNonNull(webSocketMessage.getType()) == WebSocketMessage.Type.RESPONSE_MESSAGE) {
|
||||
assert 200 == webSocketMessage.getResponseMessage().getStatus();
|
||||
@@ -206,10 +202,6 @@ class RemoteAddressFilterIntegrationTest {
|
||||
public void run(final Configuration configuration,
|
||||
final Environment environment) throws Exception {
|
||||
|
||||
environment.servlets().addFilter("RemoteAddressFilterRemoteAddress", new RemoteAddressFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, REMOTE_ADDRESS_PATH,
|
||||
WEBSOCKET_PREFIX + REMOTE_ADDRESS_PATH);
|
||||
|
||||
environment.jersey().register(new TestRemoteAddressController());
|
||||
|
||||
// WebSocket set up
|
||||
@@ -220,15 +212,14 @@ class RemoteAddressFilterIntegrationTest {
|
||||
|
||||
webSocketEnvironment.jersey().register(new TestWebSocketController());
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
|
||||
|
||||
WebSocketResourceProviderFactory<TestPrincipal> webSocketServlet = new WebSocketResourceProviderFactory<>(
|
||||
webSocketEnvironment, TestPrincipal.class, webSocketConfiguration,
|
||||
webSocketEnvironment, TestPrincipal.class,
|
||||
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
environment.servlets().addServlet("WebSocketRemoteAddress", webSocketServlet)
|
||||
.addMapping(WEBSOCKET_PREFIX + REMOTE_ADDRESS_PATH);
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), (servletContext, container) -> {
|
||||
container.addMapping(WEBSOCKET_PREFIX + REMOTE_ADDRESS_PATH, webSocketServlet);
|
||||
PriorityFilter.ensureFilter(servletContext, new RemoteAddressFilter());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.filters;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import io.dropwizard.core.Application;
|
||||
import io.dropwizard.core.Configuration;
|
||||
import io.dropwizard.core.setup.Environment;
|
||||
import io.dropwizard.testing.ConfigOverride;
|
||||
import io.dropwizard.testing.junit5.DropwizardAppExtension;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.eclipse.jetty.ee10.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.ee10.servlet.FilterMapping;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator;
|
||||
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.eclipse.jetty.http.HostPortHttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.util.Jetty;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class StripContentLengthOnConnectFilterTest {
|
||||
|
||||
private static final String WEBSOCKET_WITH_FILTER_PATH = "/websocket/filtered";
|
||||
private static final String WEBSOCKET_WITHOUT_FILTER_PATH = "/websocket/unfiltered";
|
||||
|
||||
private static final DropwizardAppExtension<Configuration> EXTENSION =
|
||||
new DropwizardAppExtension<>(TestApplicationWithFilter.class, null,
|
||||
ConfigOverride.config("server.applicationConnectors[0].type", "h2c"),
|
||||
ConfigOverride.config("server.applicationConnectors[0].port", "0"));
|
||||
|
||||
|
||||
@Test
|
||||
void contentLengthIsStrippedOnUpgrade() throws Exception {
|
||||
final HttpFields fields = sendUpgradeRequest(WEBSOCKET_WITH_FILTER_PATH);
|
||||
assertNull(fields.getField("content-length"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentLengthIncorrectlyIncludedOnUpgrade() throws Exception {
|
||||
final HttpFields fields = sendUpgradeRequest(WEBSOCKET_WITHOUT_FILTER_PATH);
|
||||
assertNotNull(fields.getField("content-length"), """
|
||||
If this fails, our jetty version no longer includes errant content-lengths on H2 connect responses.
|
||||
StripContentLengthOnConnectFilter can now be removed.
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void versionCheck() {
|
||||
assertEquals("12.1.5", Jetty.VERSION, "This class can be removed with https://github.com/jetty/jetty.project/issues/15074, likely 12.1.10");
|
||||
}
|
||||
|
||||
private static HttpFields sendUpgradeRequest(final String path) throws Exception {
|
||||
final int port = EXTENSION.getLocalPort();
|
||||
try (final HTTP2Client client = new HTTP2Client()) {
|
||||
client.start();
|
||||
final Session session =
|
||||
client.connect(new InetSocketAddress("localhost", port), new Session.Listener() {}).join();
|
||||
final HttpFields requestFields = HttpFields.build().put("sec-websocket-version", "13");
|
||||
final HostPortHttpField hostPort = new HostPortHttpField("localhost:" + port);
|
||||
final MetaData.ConnectRequest connect =
|
||||
new MetaData.ConnectRequest(HttpScheme.HTTP, hostPort, path, requestFields, "websocket");
|
||||
final HeadersFrame headersFrame = new HeadersFrame(connect, null, false);
|
||||
|
||||
final CompletableFuture<MetaData.Response> responseFuture = new CompletableFuture<>();
|
||||
Stream.Listener streamListener = new Stream.Listener() {
|
||||
@Override
|
||||
public void onHeaders(Stream stream, HeadersFrame frame) {
|
||||
if (frame.getMetaData().isResponse()) {
|
||||
responseFuture.complete((MetaData.Response) frame.getMetaData());
|
||||
}
|
||||
}
|
||||
};
|
||||
session.newStream(headersFrame, streamListener).get(5, TimeUnit.SECONDS);
|
||||
final MetaData.Response response = responseFuture.get(5, TimeUnit.SECONDS);
|
||||
return response.getHttpFields();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoopWebSocket implements org.eclipse.jetty.websocket.api.Session.Listener.AutoDemanding {}
|
||||
|
||||
public static class TestApplicationWithFilter extends Application<Configuration> {
|
||||
|
||||
@Override
|
||||
public void run(final Configuration configuration, final Environment environment) throws Exception {
|
||||
final JettyWebSocketCreator jwsc = (_, _) -> new NoopWebSocket();
|
||||
JettyWebSocketServletContainerInitializer.configure(
|
||||
environment.getApplicationContext(),
|
||||
(servletContext, container) -> {
|
||||
|
||||
container.addMapping(WEBSOCKET_WITH_FILTER_PATH, jwsc);
|
||||
container.addMapping(WEBSOCKET_WITHOUT_FILTER_PATH, jwsc);
|
||||
ensureFilter(servletContext, WEBSOCKET_WITH_FILTER_PATH, StripContentLengthOnConnectFilter.class);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureFilter(
|
||||
final ServletContext servletContext,
|
||||
final String pathSpec,
|
||||
final Class<? extends Filter> filterClass) {
|
||||
|
||||
final ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext);
|
||||
final ServletHandler servletHandler = contextHandler.getDescendant(ServletHandler.class);
|
||||
final FilterHolder holder = new FilterHolder(filterClass);
|
||||
final FilterMapping mapping = new FilterMapping();
|
||||
mapping.setFilterName(holder.getName());
|
||||
mapping.setPathSpec(pathSpec);
|
||||
mapping.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
|
||||
servletHandler.prependFilter(holder);
|
||||
servletHandler.prependFilterMapping(mapping);
|
||||
}
|
||||
}
|
||||
+61
-71
@@ -28,7 +28,6 @@ import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.InternalServerErrorException;
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
@@ -45,7 +44,6 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -55,25 +53,28 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import javax.security.auth.Subject;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.util.component.Container;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.EventsHandler;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.whispersystems.textsecuregcm.filters.PriorityFilter;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||
@@ -81,7 +82,8 @@ import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class MetricsHttpChannelListenerIntegrationTest {
|
||||
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
||||
class MetricsHttpEventHandlerIntegrationTest {
|
||||
|
||||
private static final TrafficSource TRAFFIC_SOURCE = TrafficSource.HTTP;
|
||||
private static final MeterRegistry METER_REGISTRY = mock(MeterRegistry.class);
|
||||
@@ -91,7 +93,7 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
private static final AtomicReference<CountDownLatch> COUNT_DOWN_LATCH_FUTURE_REFERENCE = new AtomicReference<>();
|
||||
|
||||
private static final DropwizardAppExtension<Configuration> EXTENSION = new DropwizardAppExtension<>(
|
||||
MetricsHttpChannelListenerIntegrationTest.TestApplication.class);
|
||||
MetricsHttpEventHandlerIntegrationTest.TestApplication.class);
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
@@ -112,9 +114,9 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
|
||||
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
|
||||
final Map<String, Counter> counterMap = Map.of(
|
||||
MetricsHttpChannelListener.REQUEST_COUNTER_NAME, REQUEST_COUNTER,
|
||||
MetricsHttpChannelListener.RESPONSE_BYTES_COUNTER_NAME, RESPONSE_BYTES_COUNTER,
|
||||
MetricsHttpChannelListener.REQUEST_BYTES_COUNTER_NAME, REQUEST_BYTES_COUNTER
|
||||
MetricsHttpEventHandler.REQUEST_COUNTER_NAME, REQUEST_COUNTER,
|
||||
MetricsHttpEventHandler.RESPONSE_BYTES_COUNTER_NAME, RESPONSE_BYTES_COUNTER,
|
||||
MetricsHttpEventHandler.REQUEST_BYTES_COUNTER_NAME, REQUEST_BYTES_COUNTER
|
||||
);
|
||||
when(METER_REGISTRY.counter(anyString(), any(Iterable.class)))
|
||||
.thenAnswer(a -> counterMap.getOrDefault(a.getArgument(0, String.class), mock(Counter.class)));
|
||||
@@ -148,7 +150,7 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
|
||||
assertTrue(countDownLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
|
||||
verify(METER_REGISTRY).counter(eq(MetricsHttpChannelListener.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
verify(METER_REGISTRY).counter(eq(MetricsHttpEventHandler.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
verify(REQUEST_COUNTER).increment();
|
||||
|
||||
final Iterable<Tag> tagIterable = tagCaptor.getValue();
|
||||
@@ -159,10 +161,10 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
}
|
||||
|
||||
assertEquals(Set.of(
|
||||
Tag.of(MetricsHttpChannelListener.PATH_TAG, expectedTagPath),
|
||||
Tag.of(MetricsHttpChannelListener.METHOD_TAG, "GET"),
|
||||
Tag.of(MetricsHttpChannelListener.STATUS_CODE_TAG, String.valueOf(expectedStatus)),
|
||||
Tag.of(MetricsHttpChannelListener.TRAFFIC_SOURCE_TAG, TRAFFIC_SOURCE.name().toLowerCase()),
|
||||
Tag.of(MetricsHttpEventHandler.PATH_TAG, expectedTagPath),
|
||||
Tag.of(MetricsHttpEventHandler.METHOD_TAG, "GET"),
|
||||
Tag.of(MetricsHttpEventHandler.STATUS_CODE_TAG, String.valueOf(expectedStatus)),
|
||||
Tag.of(MetricsHttpEventHandler.TRAFFIC_SOURCE_TAG, TRAFFIC_SOURCE.name().toLowerCase()),
|
||||
Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")),
|
||||
tags);
|
||||
}
|
||||
@@ -186,7 +188,7 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
|
||||
@Test
|
||||
void testWebSocketUpgrade() throws Exception {
|
||||
final ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest();
|
||||
final ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(URI.create(String.format("ws://localhost:%d%s", EXTENSION.getLocalPort(), "/v1/websocket")));
|
||||
upgradeRequest.setHeader(HttpHeaders.USER_AGENT, "Signal-Android/4.53.7 (Android 8.1)");
|
||||
|
||||
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
@@ -194,24 +196,18 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
|
||||
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
|
||||
final Map<String, Counter> counterMap = Map.of(
|
||||
MetricsHttpChannelListener.REQUEST_COUNTER_NAME, REQUEST_COUNTER,
|
||||
MetricsHttpChannelListener.RESPONSE_BYTES_COUNTER_NAME, RESPONSE_BYTES_COUNTER,
|
||||
MetricsHttpChannelListener.REQUEST_BYTES_COUNTER_NAME, REQUEST_BYTES_COUNTER
|
||||
MetricsHttpEventHandler.REQUEST_COUNTER_NAME, REQUEST_COUNTER,
|
||||
MetricsHttpEventHandler.RESPONSE_BYTES_COUNTER_NAME, RESPONSE_BYTES_COUNTER,
|
||||
MetricsHttpEventHandler.REQUEST_BYTES_COUNTER_NAME, REQUEST_BYTES_COUNTER
|
||||
);
|
||||
when(METER_REGISTRY.counter(anyString(), any(Iterable.class)))
|
||||
.thenAnswer(a -> counterMap.getOrDefault(a.getArgument(0, String.class), mock(Counter.class)));
|
||||
|
||||
client.connect(new WebSocketListener() {
|
||||
@Override
|
||||
public void onWebSocketConnect(final Session session) {
|
||||
session.close(1000, "OK");
|
||||
}
|
||||
},
|
||||
URI.create(String.format("ws://localhost:%d%s", EXTENSION.getLocalPort(), "/v1/websocket")), upgradeRequest);
|
||||
client.connect(new AutoClosingWebSocketSessionListener(), upgradeRequest);
|
||||
|
||||
assertTrue(countDownLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
|
||||
verify(METER_REGISTRY).counter(eq(MetricsHttpChannelListener.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
verify(METER_REGISTRY).counter(eq(MetricsHttpEventHandler.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
verify(REQUEST_COUNTER).increment();
|
||||
|
||||
final Iterable<Tag> tagIterable = tagCaptor.getValue();
|
||||
@@ -222,10 +218,10 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
}
|
||||
|
||||
assertEquals(Set.of(
|
||||
Tag.of(MetricsHttpChannelListener.PATH_TAG, "/v1/websocket"),
|
||||
Tag.of(MetricsHttpChannelListener.METHOD_TAG, "GET"),
|
||||
Tag.of(MetricsHttpChannelListener.STATUS_CODE_TAG, String.valueOf(101)),
|
||||
Tag.of(MetricsHttpChannelListener.TRAFFIC_SOURCE_TAG, TRAFFIC_SOURCE.name().toLowerCase()),
|
||||
Tag.of(MetricsHttpEventHandler.PATH_TAG, "/v1/websocket"),
|
||||
Tag.of(MetricsHttpEventHandler.METHOD_TAG, "GET"),
|
||||
Tag.of(MetricsHttpEventHandler.STATUS_CODE_TAG, String.valueOf(101)),
|
||||
Tag.of(MetricsHttpEventHandler.TRAFFIC_SOURCE_TAG, TRAFFIC_SOURCE.name().toLowerCase()),
|
||||
Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")),
|
||||
tags);
|
||||
}
|
||||
@@ -248,9 +244,9 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
|
||||
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
|
||||
final Map<String, Counter> counterMap = Map.of(
|
||||
MetricsHttpChannelListener.REQUEST_COUNTER_NAME, REQUEST_COUNTER,
|
||||
MetricsHttpChannelListener.RESPONSE_BYTES_COUNTER_NAME, RESPONSE_BYTES_COUNTER,
|
||||
MetricsHttpChannelListener.REQUEST_BYTES_COUNTER_NAME, REQUEST_BYTES_COUNTER
|
||||
MetricsHttpEventHandler.REQUEST_COUNTER_NAME, REQUEST_COUNTER,
|
||||
MetricsHttpEventHandler.RESPONSE_BYTES_COUNTER_NAME, RESPONSE_BYTES_COUNTER,
|
||||
MetricsHttpEventHandler.REQUEST_BYTES_COUNTER_NAME, REQUEST_BYTES_COUNTER
|
||||
);
|
||||
when(METER_REGISTRY.counter(anyString(), any(Iterable.class)))
|
||||
.thenAnswer(a -> counterMap.getOrDefault(a.getArgument(0, String.class), mock(Counter.class)));
|
||||
@@ -263,7 +259,7 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
|
||||
assertTrue(countDownLatch.await(1000, TimeUnit.MILLISECONDS));
|
||||
|
||||
verify(METER_REGISTRY).counter(eq(MetricsHttpChannelListener.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
verify(METER_REGISTRY).counter(eq(MetricsHttpEventHandler.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
verify(REQUEST_COUNTER).increment();
|
||||
|
||||
final Iterable<Tag> tagIterable = tagCaptor.getValue();
|
||||
@@ -273,7 +269,7 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
tags.add(tag);
|
||||
}
|
||||
|
||||
assertFalse(tags.contains(Tag.of(MetricsHttpChannelListener.METHOD_TAG, "ANNOY")));
|
||||
assertFalse(tags.contains(Tag.of(MetricsHttpEventHandler.METHOD_TAG, "ANNOY")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,17 +279,16 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
public void run(final Configuration configuration,
|
||||
final Environment environment) throws Exception {
|
||||
|
||||
final MetricsHttpChannelListener metricsHttpChannelListener = new MetricsHttpChannelListener(
|
||||
METER_REGISTRY,
|
||||
mock(ClientReleaseManager.class),
|
||||
Set.of("/v1/websocket")
|
||||
);
|
||||
MetricsHttpEventHandler.configure(environment, METER_REGISTRY, mock(ClientReleaseManager.class), Set.of("/v1/websocket"));
|
||||
|
||||
metricsHttpChannelListener.configure(environment);
|
||||
environment.lifecycle().addEventListener(new TestListener(COUNT_DOWN_LATCH_FUTURE_REFERENCE));
|
||||
|
||||
environment.servlets().addFilter("RemoteAddressFilter", new RemoteAddressFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
environment.lifecycle().addEventListener(new LifeCycle.Listener() {
|
||||
@Override
|
||||
public void lifeCycleStarting(final LifeCycle event) {
|
||||
if (event instanceof Server server) {
|
||||
server.setHandler(new TestListener(server.getHandler(), COUNT_DOWN_LATCH_FUTURE_REFERENCE));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
environment.jersey().register(new TestResource());
|
||||
environment.jersey().register(new TestAuthFilter());
|
||||
@@ -306,14 +301,15 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
|
||||
webSocketEnvironment.jersey().register(new TestResource());
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
|
||||
|
||||
WebSocketResourceProviderFactory<TestPrincipal> webSocketServlet = new WebSocketResourceProviderFactory<>(
|
||||
webSocketEnvironment, TestPrincipal.class, webSocketConfiguration,
|
||||
webSocketEnvironment, TestPrincipal.class,
|
||||
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
environment.servlets().addServlet("WebSocket", webSocketServlet)
|
||||
.addMapping("/v1/websocket");
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(),
|
||||
(servletContext, container) -> {
|
||||
container.addMapping("/v1/websocket", webSocketServlet);
|
||||
PriorityFilter.ensureFilter(servletContext, new RemoteAddressFilter());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,36 +325,23 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple listener to signal that {@link HttpChannel.Listener} has completed its work, since its onComplete() is on
|
||||
* A simple listener to signal that {@link EventsHandler} has completed its work, since its onComplete() is on
|
||||
* a different thread from the one that sends the response, creating a race condition between the listener and the
|
||||
* test assertions
|
||||
*/
|
||||
static class TestListener implements HttpChannel.Listener, Container.Listener, LifeCycle.Listener {
|
||||
static class TestListener extends EventsHandler {
|
||||
|
||||
private final AtomicReference<CountDownLatch> completableFutureAtomicReference;
|
||||
|
||||
TestListener(AtomicReference<CountDownLatch> countDownLatchReference) {
|
||||
|
||||
TestListener(final Handler handler, AtomicReference<CountDownLatch> countDownLatchReference) {
|
||||
super(handler);
|
||||
this.completableFutureAtomicReference = countDownLatchReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(final Request request) {
|
||||
public void onComplete(Request request, int status, HttpFields headers, Throwable failure) {
|
||||
completableFutureAtomicReference.get().countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beanAdded(final Container parent, final Object child) {
|
||||
if (child instanceof Connector connector) {
|
||||
connector.addBean(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beanRemoved(final Container parent, final Object child) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Path("/v1/test")
|
||||
@@ -400,4 +383,11 @@ class MetricsHttpChannelListenerIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
public static class AutoClosingWebSocketSessionListener implements Session.Listener.AutoDemanding {
|
||||
@Override
|
||||
public void onWebSocketOpen(final Session session) {
|
||||
session.close(1000, "OK", Callback.NOOP);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+87
-35
@@ -20,24 +20,33 @@ import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.util.Collections;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.ContainerResponse;
|
||||
import org.glassfish.jersey.server.ExtendedUriInfo;
|
||||
import org.glassfish.jersey.uri.UriTemplate;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junitpioneer.jupiter.cartesian.CartesianTest;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
class MetricsHttpChannelListenerTest {
|
||||
class MetricsHttpEventHandlerTest {
|
||||
private final static String USER_AGENT = "Signal-Android/6.53.7 (Android 8.1)";
|
||||
|
||||
private MeterRegistry meterRegistry;
|
||||
private Counter requestCounter;
|
||||
@@ -45,7 +54,7 @@ class MetricsHttpChannelListenerTest {
|
||||
private Counter responseBytesCounter;
|
||||
private Counter requestBytesCounter;
|
||||
private ClientReleaseManager clientReleaseManager;
|
||||
private MetricsHttpChannelListener listener;
|
||||
private MetricsHttpEventHandler listener;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
@@ -55,27 +64,28 @@ class MetricsHttpChannelListenerTest {
|
||||
responseBytesCounter = mock(Counter.class);
|
||||
requestBytesCounter = mock(Counter.class);
|
||||
|
||||
when(meterRegistry.counter(eq(MetricsHttpChannelListener.REQUEST_COUNTER_NAME), any(Iterable.class)))
|
||||
when(meterRegistry.counter(eq(MetricsHttpEventHandler.REQUEST_COUNTER_NAME), any(Iterable.class)))
|
||||
.thenReturn(requestCounter);
|
||||
|
||||
when(meterRegistry.counter(eq(MetricsHttpChannelListener.REQUESTS_BY_VERSION_COUNTER_NAME), any(Tags.class)))
|
||||
when(meterRegistry.counter(eq(MetricsHttpEventHandler.REQUESTS_BY_VERSION_COUNTER_NAME), any(Tags.class)))
|
||||
.thenReturn(requestsByVersionCounter);
|
||||
|
||||
when(meterRegistry.counter(eq(MetricsHttpChannelListener.RESPONSE_BYTES_COUNTER_NAME), any(Iterable.class)))
|
||||
when(meterRegistry.counter(eq(MetricsHttpEventHandler.RESPONSE_BYTES_COUNTER_NAME), any(Iterable.class)))
|
||||
.thenReturn(responseBytesCounter);
|
||||
|
||||
when(meterRegistry.counter(eq(MetricsHttpChannelListener.REQUEST_BYTES_COUNTER_NAME), any(Iterable.class)))
|
||||
when(meterRegistry.counter(eq(MetricsHttpEventHandler.REQUEST_BYTES_COUNTER_NAME), any(Iterable.class)))
|
||||
.thenReturn(requestBytesCounter);
|
||||
|
||||
clientReleaseManager = mock(ClientReleaseManager.class);
|
||||
|
||||
listener = new MetricsHttpChannelListener(meterRegistry, clientReleaseManager, Collections.emptySet());
|
||||
listener = new MetricsHttpEventHandler(null, meterRegistry, clientReleaseManager, Set.of("/test"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
@CartesianTest
|
||||
@SuppressWarnings("unchecked")
|
||||
void testRequests(final boolean versionActive) {
|
||||
void testRequests(@CartesianTest.Values(booleans = {true, false}) final boolean pathFromFilter,
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean versionActive) {
|
||||
|
||||
final String path = "/test";
|
||||
final String method = "GET";
|
||||
final int statusCode = 200;
|
||||
@@ -85,30 +95,40 @@ class MetricsHttpChannelListenerTest {
|
||||
|
||||
final Request request = mock(Request.class);
|
||||
when(request.getMethod()).thenReturn(method);
|
||||
when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn("Signal-Android/6.53.7 (Android 8.1)");
|
||||
|
||||
final HttpFields.Mutable requestHeaders = HttpFields.build();
|
||||
requestHeaders.put(HttpHeader.USER_AGENT, USER_AGENT);
|
||||
when(request.getHeaders()).thenReturn(requestHeaders);
|
||||
when(request.getHttpURI()).thenReturn(httpUri);
|
||||
|
||||
if (pathFromFilter) {
|
||||
when(request.getAttribute(MetricsHttpEventHandler.REQUEST_INFO_PROPERTY_NAME))
|
||||
.thenReturn(new MetricsHttpEventHandler.RequestInfo(path, method, USER_AGENT));
|
||||
} else {
|
||||
when(request.setAttribute(eq(MetricsHttpEventHandler.REQUEST_INFO_PROPERTY_NAME), any())).thenAnswer(invocation -> {
|
||||
when(request.getAttribute(MetricsHttpEventHandler.REQUEST_INFO_PROPERTY_NAME))
|
||||
.thenReturn(invocation.getArgument(1));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
final Response response = mock(Response.class);
|
||||
when(response.getStatus()).thenReturn(statusCode);
|
||||
when(clientReleaseManager.isVersionActive(any(), any())).thenReturn(versionActive);
|
||||
|
||||
when(response.getContentCount()).thenReturn(1024L);
|
||||
when(request.getResponse()).thenReturn(response);
|
||||
when(request.getContentRead()).thenReturn(512L);
|
||||
final ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class);
|
||||
when(request.getAttribute(MetricsHttpChannelListener.URI_INFO_PROPERTY_NAME)).thenReturn(extendedUriInfo);
|
||||
when(extendedUriInfo.getMatchedTemplates()).thenReturn(List.of(new UriTemplate(path)));
|
||||
when(clientReleaseManager.isVersionActive(any(), any())).thenReturn(versionActive);
|
||||
|
||||
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
|
||||
|
||||
listener.onComplete(request);
|
||||
listener.onRequestRead(request, Content.Chunk.from(ByteBuffer.allocate(512), true));
|
||||
listener.onResponseWrite(request, true, ByteBuffer.allocate(1024));
|
||||
listener.onComplete(request, statusCode, requestHeaders, null);
|
||||
|
||||
verify(requestCounter).increment();
|
||||
|
||||
verify(responseBytesCounter).increment(1024L);
|
||||
verify(requestBytesCounter).increment(512L);
|
||||
|
||||
verify(meterRegistry).counter(eq(MetricsHttpChannelListener.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
verify(meterRegistry).counter(eq(MetricsHttpEventHandler.REQUEST_COUNTER_NAME), tagCaptor.capture());
|
||||
|
||||
final Set<Tag> tags = new HashSet<>();
|
||||
for (final Tag tag : tagCaptor.getValue()) {
|
||||
@@ -116,10 +136,10 @@ class MetricsHttpChannelListenerTest {
|
||||
}
|
||||
|
||||
final Set<Tag> expectedTags = new HashSet<>(Set.of(
|
||||
Tag.of(MetricsHttpChannelListener.PATH_TAG, path),
|
||||
Tag.of(MetricsHttpChannelListener.METHOD_TAG, method),
|
||||
Tag.of(MetricsHttpChannelListener.STATUS_CODE_TAG, String.valueOf(statusCode)),
|
||||
Tag.of(MetricsHttpChannelListener.TRAFFIC_SOURCE_TAG, TrafficSource.HTTP.name().toLowerCase()),
|
||||
Tag.of(MetricsHttpEventHandler.PATH_TAG, path),
|
||||
Tag.of(MetricsHttpEventHandler.METHOD_TAG, method),
|
||||
Tag.of(MetricsHttpEventHandler.STATUS_CODE_TAG, String.valueOf(statusCode)),
|
||||
Tag.of(MetricsHttpEventHandler.TRAFFIC_SOURCE_TAG, TrafficSource.HTTP.name().toLowerCase()),
|
||||
Tag.of(UserAgentTagUtil.PLATFORM_TAG, "android")));
|
||||
|
||||
if (versionActive) {
|
||||
@@ -143,23 +163,22 @@ class MetricsHttpChannelListenerTest {
|
||||
|
||||
final Request request = mock(Request.class);
|
||||
when(request.getMethod()).thenReturn(method);
|
||||
when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn("Signal-Android/6.53.7 (Android 8.1)");
|
||||
final HttpFields.Mutable requestHeaders = HttpFields.build();
|
||||
requestHeaders.put(HttpHeader.USER_AGENT, USER_AGENT);
|
||||
when(request.getHeaders()).thenReturn(requestHeaders);
|
||||
when(request.getHttpURI()).thenReturn(httpUri);
|
||||
when(request.getAttribute(MetricsHttpEventHandler.REQUEST_INFO_PROPERTY_NAME))
|
||||
.thenReturn(new MetricsHttpEventHandler.RequestInfo(path, method, USER_AGENT));
|
||||
|
||||
final Response response = mock(Response.class);
|
||||
when(response.getStatus()).thenReturn(statusCode);
|
||||
when(response.getContentCount()).thenReturn(1024L);
|
||||
when(request.getResponse()).thenReturn(response);
|
||||
when(request.getContentRead()).thenReturn(512L);
|
||||
final ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class);
|
||||
when(request.getAttribute(MetricsHttpChannelListener.URI_INFO_PROPERTY_NAME)).thenReturn(extendedUriInfo);
|
||||
when(extendedUriInfo.getMatchedTemplates()).thenReturn(List.of(new UriTemplate(path)));
|
||||
|
||||
listener.onComplete(request);
|
||||
listener.onRequestRead(request, Content.Chunk.from(ByteBuffer.allocate(512), true));
|
||||
listener.onResponseWrite(request, true, ByteBuffer.allocate(1024));
|
||||
listener.onComplete(request, statusCode, requestHeaders, null);
|
||||
|
||||
if (versionActive) {
|
||||
final ArgumentCaptor<Tags> tagCaptor = ArgumentCaptor.forClass(Tags.class);
|
||||
verify(meterRegistry).counter(eq(MetricsHttpChannelListener.REQUESTS_BY_VERSION_COUNTER_NAME),
|
||||
verify(meterRegistry).counter(eq(MetricsHttpEventHandler.REQUESTS_BY_VERSION_COUNTER_NAME),
|
||||
tagCaptor.capture());
|
||||
final Set<Tag> tags = new HashSet<>();
|
||||
tags.clear();
|
||||
@@ -178,7 +197,7 @@ class MetricsHttpChannelListenerTest {
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void normalizeMethod(@Nullable final String originalMethod, final String expectedMethod) {
|
||||
assertEquals(expectedMethod, MetricsHttpChannelListener.normalizeMethod(originalMethod));
|
||||
assertEquals(expectedMethod, MetricsHttpEventHandler.normalizeMethod(originalMethod));
|
||||
}
|
||||
|
||||
private static List<Arguments> normalizeMethod() {
|
||||
@@ -190,4 +209,37 @@ class MetricsHttpChannelListenerTest {
|
||||
Arguments.arguments("get", "get")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResponseFilterSetsRequestInfo() {
|
||||
final ContainerRequest request = mock(ContainerRequest.class);
|
||||
|
||||
final ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class);
|
||||
when(extendedUriInfo.getMatchedTemplates()).thenReturn(List.of(new UriTemplate("/test")));
|
||||
when(request.getMethod()).thenReturn("GET");
|
||||
when(request.getHeaders()).thenReturn(null);
|
||||
when(request.getUriInfo()).thenReturn(extendedUriInfo);
|
||||
when(request.getHeaderString(HttpHeaders.USER_AGENT)).thenReturn(USER_AGENT);
|
||||
|
||||
new MetricsHttpEventHandler.SetInfoRequestFilter().filter(request, mock(ContainerResponse.class));
|
||||
|
||||
verify(request).setProperty(
|
||||
eq(MetricsHttpEventHandler.REQUEST_INFO_PROPERTY_NAME),
|
||||
eq(new MetricsHttpEventHandler.RequestInfo("/test", "GET", USER_AGENT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResponseFilterModifiesRequestInfo() {
|
||||
final MetricsHttpEventHandler.RequestInfo requestInfo =
|
||||
new MetricsHttpEventHandler.RequestInfo("unknown", "POST", USER_AGENT);
|
||||
|
||||
final ContainerRequest request = mock(ContainerRequest.class);
|
||||
when(request.getProperty(MetricsHttpEventHandler.REQUEST_INFO_PROPERTY_NAME)).thenReturn(requestInfo);
|
||||
final ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class);
|
||||
when(extendedUriInfo.getMatchedTemplates()).thenReturn(List.of(new UriTemplate("/test")));
|
||||
when(request.getUriInfo()).thenReturn(extendedUriInfo);
|
||||
new MetricsHttpEventHandler.SetInfoRequestFilter().filter(request, mock(ContainerResponse.class));
|
||||
|
||||
assertEquals(new MetricsHttpEventHandler.RequestInfo("/test", "POST", USER_AGENT), requestInfo);
|
||||
}
|
||||
}
|
||||
+10
-17
@@ -36,10 +36,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.glassfish.jersey.server.ApplicationHandler;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.ContainerResponse;
|
||||
@@ -175,11 +174,9 @@ class MetricsRequestEventListenerTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
final Session session = mock(Session.class);
|
||||
final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
final UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn("Signal-Android/4.53.7 (Android 8.1)");
|
||||
when(request.getHeaders()).thenReturn(Map.of(HttpHeaders.USER_AGENT, List.of("Signal-Android/4.53.7 (Android 8.1)")));
|
||||
|
||||
@@ -191,15 +188,15 @@ class MetricsRequestEventListenerTest {
|
||||
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_BYTES_COUNTER_NAME), any(Iterable.class)))
|
||||
.thenReturn(requestBytesCounter);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
final ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -244,11 +241,9 @@ class MetricsRequestEventListenerTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
final Session session = mock(Session.class);
|
||||
final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
final UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
|
||||
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))).thenReturn(
|
||||
@@ -258,15 +253,15 @@ class MetricsRequestEventListenerTest {
|
||||
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_BYTES_COUNTER_NAME), any(Iterable.class)))
|
||||
.thenReturn(requestBytesCounter);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
final byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
final ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -314,11 +309,9 @@ class MetricsRequestEventListenerTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
final Session session = mock(Session.class);
|
||||
final RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
final UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
final ArgumentCaptor<Iterable<Tag>> tagCaptor = ArgumentCaptor.forClass(Iterable.class);
|
||||
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_COUNTER_NAME), any(Iterable.class))).thenReturn(
|
||||
@@ -328,15 +321,15 @@ class MetricsRequestEventListenerTest {
|
||||
when(meterRegistry.counter(eq(MetricsRequestEventListener.REQUEST_BYTES_COUNTER_NAME), any(Iterable.class)))
|
||||
.thenReturn(requestBytesCounter);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
final byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
final ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
|
||||
+6
-8
@@ -149,15 +149,13 @@ class TlsCertificateExpirationUtilTest {
|
||||
|
||||
@Test
|
||||
void test() throws Exception {
|
||||
try (Resource keystore = TestResource.fromBase64Mime("keystore", KEYSTORE_BASE64)) {
|
||||
final Resource keystore = TestResource.fromBase64Mime("keystore", KEYSTORE_BASE64);
|
||||
final KeyStore keyStore = CertificateUtils.getKeyStore(keystore, "PKCS12", null, KEYSTORE_PASSWORD);
|
||||
|
||||
final KeyStore keyStore = CertificateUtils.getKeyStore(keystore, "PKCS12", null, KEYSTORE_PASSWORD);
|
||||
|
||||
final Map<String, Instant> expected = Map.of(
|
||||
"localhost:EdDSA", EDDSA_EXPIRATION,
|
||||
"localhost:RSA", RSA_EXPIRATION);
|
||||
assertEquals(expected, TlsCertificateExpirationUtil.getIdentifiersAndExpirations(keyStore, KEYSTORE_PASSWORD));
|
||||
}
|
||||
final Map<String, Instant> expected = Map.of(
|
||||
"localhost:EdDSA", EDDSA_EXPIRATION,
|
||||
"localhost:RSA", RSA_EXPIRATION);
|
||||
assertEquals(expected, TlsCertificateExpirationUtil.getIdentifiersAndExpirations(keyStore, KEYSTORE_PASSWORD));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-17
@@ -4,14 +4,6 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.tests.util;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.whispersystems.websocket.messages.WebSocketMessage;
|
||||
import org.whispersystems.websocket.messages.WebSocketMessageFactory;
|
||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -19,8 +11,14 @@ import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.whispersystems.websocket.messages.WebSocketMessage;
|
||||
import org.whispersystems.websocket.messages.WebSocketMessageFactory;
|
||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
import org.whispersystems.websocket.messages.protobuf.ProtobufWebSocketMessageFactory;
|
||||
|
||||
public class TestWebsocketListener implements WebSocketListener {
|
||||
public class TestWebsocketListener implements Session.Listener.AutoDemanding {
|
||||
|
||||
private final AtomicLong requestId = new AtomicLong();
|
||||
private final CompletableFuture<Session> started = new CompletableFuture<>();
|
||||
@@ -34,7 +32,7 @@ public class TestWebsocketListener implements WebSocketListener {
|
||||
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(final Session session) {
|
||||
public void onWebSocketOpen(final Session session) {
|
||||
started.complete(session);
|
||||
|
||||
}
|
||||
@@ -63,19 +61,15 @@ public class TestWebsocketListener implements WebSocketListener {
|
||||
responseFutures.put(id, future);
|
||||
final byte[] requestBytes = messageFactory.createRequest(
|
||||
Optional.of(id), verb, requestPath, headers, body).toByteArray();
|
||||
try {
|
||||
session.getRemote().sendBytes(ByteBuffer.wrap(requestBytes));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
session.sendBinary(ByteBuffer.wrap(requestBytes), Callback.NOOP);
|
||||
return future;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(final byte[] payload, final int offset, final int length) {
|
||||
public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
|
||||
try {
|
||||
WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload, offset, length);
|
||||
WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload);
|
||||
if (Objects.requireNonNull(webSocketMessage.getType()) == WebSocketMessage.Type.RESPONSE_MESSAGE) {
|
||||
responseFutures.get(webSocketMessage.getResponseMessage().getRequestId())
|
||||
.complete(webSocketMessage.getResponseMessage());
|
||||
|
||||
+12
-38
@@ -6,12 +6,9 @@
|
||||
package org.whispersystems.textsecuregcm.util.jetty;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
|
||||
@@ -30,15 +27,16 @@ public class TestResource extends Resource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContainedIn(final Resource r) throws MalformedURLException {
|
||||
return false;
|
||||
public Path getPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
public InputStream newInputStream() {
|
||||
return new ByteArrayInputStream(data);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
@@ -50,13 +48,13 @@ public class TestResource extends Resource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() {
|
||||
return 0;
|
||||
public boolean isReadable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return 0;
|
||||
return data.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,43 +62,19 @@ public class TestResource extends Resource {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(data);
|
||||
public String getFileName() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableByteChannel getReadableByteChannel() throws IOException {
|
||||
public Resource resolve(final String subUriPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete() throws SecurityException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean renameTo(final Resource dest) throws SecurityException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] list() {
|
||||
return new String[]{name};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource addPath(final String path) throws IOException, MalformedURLException {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-7
@@ -39,10 +39,9 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.glassfish.jersey.server.ApplicationHandler;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
@@ -143,12 +142,12 @@ class LoggingUnhandledExceptionMapperTest {
|
||||
WebSocketResourceProvider<TestPrincipal> provider = createWebsocketProvider(userAgentHeader, session,
|
||||
responseFuture::complete);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory()
|
||||
.createRequest(Optional.of(111L), "GET", targetPath, new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
responseFuture.get(1, TimeUnit.SECONDS);
|
||||
|
||||
@@ -179,15 +178,13 @@ class LoggingUnhandledExceptionMapperTest {
|
||||
TestPrincipal.authenticatedTestPrincipal("foo"),
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
doAnswer(answer -> {
|
||||
responseHandler.accept(answer.getArgument(0, ByteBuffer.class));
|
||||
return null;
|
||||
}).when(remoteEndpoint).sendBytes(any(), any(WriteCallback.class));
|
||||
}).when(session).sendBinary(any(ByteBuffer.class), any(Callback.class));
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
when(request.getHeader(HttpHeaders.USER_AGENT)).thenReturn(userAgentHeader);
|
||||
when(request.getHeaders()).thenReturn(Map.of(HttpHeaders.USER_AGENT, List.of(userAgentHeader)));
|
||||
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
@@ -44,7 +44,7 @@ class WebSocketAccountAuthenticatorTest {
|
||||
|
||||
private AccountAuthenticator accountAuthenticator;
|
||||
|
||||
private UpgradeRequest upgradeRequest;
|
||||
private JettyServerUpgradeRequest upgradeRequest;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@@ -56,7 +56,7 @@ class WebSocketAccountAuthenticatorTest {
|
||||
when(accountAuthenticator.authenticate(eq(new BasicCredentials(INVALID_USER, INVALID_PASSWORD))))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
upgradeRequest = mock(UpgradeRequest.class);
|
||||
upgradeRequest = mock(JettyServerUpgradeRequest.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
||||
@@ -13,15 +13,19 @@
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-jetty-api</artifactId>
|
||||
<artifactId>jetty-websocket-jetty-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-jetty-server</artifactId>
|
||||
<artifactId>jetty-websocket-jetty-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-servlet</artifactId>
|
||||
<groupId>org.eclipse.jetty.ee10.websocket</groupId>
|
||||
<artifactId>jetty-ee10-websocket-jetty-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.ee10.websocket</groupId>
|
||||
<artifactId>jetty-ee10-websocket-servlet</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
|
||||
+7
-10
@@ -13,9 +13,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.eclipse.jetty.websocket.api.exceptions.WebSocketException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -30,15 +29,13 @@ public class WebSocketClient {
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
private final Session session;
|
||||
private final RemoteEndpoint remoteEndpoint;
|
||||
private final WebSocketMessageFactory messageFactory;
|
||||
private final Map<Long, CompletableFuture<WebSocketResponseMessage>> pendingRequestMapper;
|
||||
private final Instant created;
|
||||
|
||||
public WebSocketClient(Session session, RemoteEndpoint remoteEndpoint, WebSocketMessageFactory messageFactory,
|
||||
public WebSocketClient(Session session, WebSocketMessageFactory messageFactory,
|
||||
Map<Long, CompletableFuture<WebSocketResponseMessage>> pendingRequestMapper) {
|
||||
this.session = session;
|
||||
this.remoteEndpoint = remoteEndpoint;
|
||||
this.messageFactory = messageFactory;
|
||||
this.pendingRequestMapper = pendingRequestMapper;
|
||||
this.created = Instant.now();
|
||||
@@ -56,9 +53,9 @@ public class WebSocketClient {
|
||||
WebSocketMessage requestMessage = messageFactory.createRequest(Optional.of(requestId), verb, path, headers, body);
|
||||
|
||||
try {
|
||||
remoteEndpoint.sendBytes(ByteBuffer.wrap(requestMessage.toByteArray()), new WriteCallback() {
|
||||
session.sendBinary(ByteBuffer.wrap(requestMessage.toByteArray()), new Callback() {
|
||||
@Override
|
||||
public void writeFailed(Throwable x) {
|
||||
public void fail(Throwable x) {
|
||||
logger.debug("Write failed", x);
|
||||
pendingRequestMapper.remove(requestId);
|
||||
future.completeExceptionally(x);
|
||||
@@ -86,9 +83,9 @@ public class WebSocketClient {
|
||||
}
|
||||
|
||||
public void close(final int code, final String message) {
|
||||
session.close(code, message, new WriteCallback() {
|
||||
session.close(code, message, new Callback() {
|
||||
@Override
|
||||
public void writeFailed(final Throwable throwable) {
|
||||
public void fail(final Throwable throwable) {
|
||||
try {
|
||||
session.disconnect();
|
||||
} catch (final Exception e) {
|
||||
@@ -108,6 +105,6 @@ public class WebSocketClient {
|
||||
}
|
||||
|
||||
public SocketAddress getRemoteAddress() {
|
||||
return session.getRemoteAddress();
|
||||
return session.getRemoteSocketAddress();
|
||||
}
|
||||
}
|
||||
|
||||
+31
-22
@@ -24,11 +24,8 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.eclipse.jetty.websocket.api.exceptions.MessageTooLargeException;
|
||||
import org.glassfish.jersey.internal.MapPropertiesDelegate;
|
||||
import org.glassfish.jersey.server.ApplicationHandler;
|
||||
@@ -48,11 +45,11 @@ import org.whispersystems.websocket.setup.WebSocketConnectListener;
|
||||
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class WebSocketResourceProvider<T extends Principal> implements WebSocketListener {
|
||||
public class WebSocketResourceProvider<T extends Principal> implements Session.Listener.AutoDemanding {
|
||||
|
||||
/**
|
||||
* A static exception instance passed to outstanding requests (via {@code completeExceptionally} in
|
||||
* {@link #onWebSocketClose(int, String)}
|
||||
* {@link #onWebSocketClose}
|
||||
*/
|
||||
public static final IOException CONNECTION_CLOSED_EXCEPTION = new IOException("Connection closed!");
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketResourceProvider.class);
|
||||
@@ -70,7 +67,6 @@ public class WebSocketResourceProvider<T extends Principal> implements WebSocket
|
||||
private final int localPort;
|
||||
|
||||
private Session session;
|
||||
private RemoteEndpoint remoteEndpoint;
|
||||
private WebSocketSessionContext context;
|
||||
|
||||
private static final Set<String> EXCLUDED_UPGRADE_REQUEST_HEADERS = Set.of("connection", "upgrade");
|
||||
@@ -96,11 +92,10 @@ public class WebSocketResourceProvider<T extends Principal> implements WebSocket
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
public void onWebSocketOpen(Session session) {
|
||||
this.session = session;
|
||||
this.remoteEndpoint = session.getRemote();
|
||||
this.context = new WebSocketSessionContext(
|
||||
new WebSocketClient(session, remoteEndpoint, messageFactory, requestMap));
|
||||
new WebSocketClient(session, messageFactory, requestMap));
|
||||
this.context.setAuthenticated(reusableAuth.orElse(null));
|
||||
this.session.setIdleTimeout(idleTimeout);
|
||||
|
||||
@@ -125,9 +120,19 @@ public class WebSocketResourceProvider<T extends Principal> implements WebSocket
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(byte[] payload, int offset, int length) {
|
||||
public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
|
||||
try {
|
||||
WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload, offset, length);
|
||||
onWebSocketBinary(payload);
|
||||
callback.succeed();
|
||||
} catch (RuntimeException e) {
|
||||
callback.fail(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void onWebSocketBinary(ByteBuffer payload) {
|
||||
try {
|
||||
final WebSocketMessage webSocketMessage = messageFactory.parseMessage(payload);
|
||||
|
||||
switch (webSocketMessage.getType()) {
|
||||
case REQUEST_MESSAGE:
|
||||
@@ -147,17 +152,21 @@ public class WebSocketResourceProvider<T extends Principal> implements WebSocket
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, String reason) {
|
||||
if (context != null) {
|
||||
context.notifyClosed(statusCode, reason);
|
||||
public void onWebSocketClose(int statusCode, String reason, Callback callback) {
|
||||
try {
|
||||
if (context != null) {
|
||||
context.notifyClosed(statusCode, reason);
|
||||
|
||||
for (long requestId : requestMap.keySet()) {
|
||||
CompletableFuture<WebSocketResponseMessage> outstandingRequest = requestMap.remove(requestId);
|
||||
for (long requestId : requestMap.keySet()) {
|
||||
CompletableFuture<WebSocketResponseMessage> outstandingRequest = requestMap.remove(requestId);
|
||||
|
||||
if (outstandingRequest != null) {
|
||||
outstandingRequest.completeExceptionally(CONNECTION_CLOSED_EXCEPTION);
|
||||
if (outstandingRequest != null) {
|
||||
outstandingRequest.completeExceptionally(CONNECTION_CLOSED_EXCEPTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
callback.succeed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +278,7 @@ public class WebSocketResourceProvider<T extends Principal> implements WebSocket
|
||||
}
|
||||
|
||||
private void close(Session session, int status, String message) {
|
||||
session.close(status, message);
|
||||
session.close(status, message, Callback.NOOP);
|
||||
}
|
||||
|
||||
private void sendResponse(WebSocketRequestMessage requestMessage, ContainerResponse response,
|
||||
@@ -288,7 +297,7 @@ public class WebSocketResourceProvider<T extends Principal> implements WebSocket
|
||||
Optional.ofNullable(body))
|
||||
.toByteArray();
|
||||
|
||||
remoteEndpoint.sendBytes(ByteBuffer.wrap(responseBytes), WriteCallback.NOOP);
|
||||
session.sendBinary(ByteBuffer.wrap(responseBytes), Callback.NOOP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +309,7 @@ public class WebSocketResourceProvider<T extends Principal> implements WebSocket
|
||||
getHeaderList(error.getStringHeaders()),
|
||||
Optional.empty());
|
||||
|
||||
remoteEndpoint.sendBytes(ByteBuffer.wrap(response.toByteArray()), WriteCallback.NOOP);
|
||||
session.sendBinary(ByteBuffer.wrap(response.toByteArray()), Callback.NOOP);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-18
@@ -13,11 +13,9 @@ import java.security.Principal;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketServlet;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator;
|
||||
import org.glassfish.jersey.CommonProperties;
|
||||
import org.glassfish.jersey.server.ApplicationHandler;
|
||||
import org.slf4j.Logger;
|
||||
@@ -25,23 +23,20 @@ import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.websocket.auth.InvalidCredentialsException;
|
||||
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
|
||||
import org.whispersystems.websocket.auth.WebsocketAuthValueFactoryProvider;
|
||||
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||
import org.whispersystems.websocket.session.WebSocketSessionContextValueFactoryProvider;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
public class WebSocketResourceProviderFactory<T extends Principal> extends JettyWebSocketServlet implements
|
||||
JettyWebSocketCreator {
|
||||
public class WebSocketResourceProviderFactory<T extends Principal> implements JettyWebSocketCreator {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketResourceProviderFactory.class);
|
||||
|
||||
private final WebSocketEnvironment<T> environment;
|
||||
private final ApplicationHandler jerseyApplicationHandler;
|
||||
private final WebSocketConfiguration configuration;
|
||||
|
||||
private final String remoteAddressPropertyName;
|
||||
|
||||
public WebSocketResourceProviderFactory(WebSocketEnvironment<T> environment, Class<T> principalClass,
|
||||
WebSocketConfiguration configuration, String remoteAddressPropertyName) {
|
||||
String remoteAddressPropertyName) {
|
||||
this.environment = environment;
|
||||
|
||||
environment.jersey().register(new WebSocketSessionContextValueFactoryProvider.Binder());
|
||||
@@ -55,7 +50,6 @@ public class WebSocketResourceProviderFactory<T extends Principal> extends Jetty
|
||||
|
||||
this.jerseyApplicationHandler = new ApplicationHandler(environment.jersey());
|
||||
|
||||
this.configuration = configuration;
|
||||
this.remoteAddressPropertyName = remoteAddressPropertyName;
|
||||
}
|
||||
|
||||
@@ -91,6 +85,7 @@ public class WebSocketResourceProviderFactory<T extends Principal> extends Jetty
|
||||
// Authentication may fail for non-incorrect-credential reasons (e.g. we couldn't read from the account database).
|
||||
// If that happens, we don't want to incorrectly tell clients that they provided bad credentials.
|
||||
logger.warn("Authentication failure", e);
|
||||
|
||||
try {
|
||||
response.sendError(500, "Failure");
|
||||
} catch (final IOException ignored) {
|
||||
@@ -99,13 +94,6 @@ public class WebSocketResourceProviderFactory<T extends Principal> extends Jetty
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(JettyWebSocketServletFactory factory) {
|
||||
factory.setCreator(this);
|
||||
factory.setMaxBinaryMessageSize(configuration.getMaxBinaryMessageSize());
|
||||
factory.setMaxTextMessageSize(configuration.getMaxTextMessageSize());
|
||||
}
|
||||
|
||||
private String getRemoteAddress(JettyServerUpgradeRequest request) {
|
||||
final String remoteAddress = (String) request.getHttpServletRequest().getAttribute(remoteAddressPropertyName);
|
||||
if (StringUtils.isBlank(remoteAddress)) {
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ package org.whispersystems.websocket.auth;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeResponse;
|
||||
|
||||
public interface AuthenticatedWebSocketUpgradeFilter<T extends Principal> {
|
||||
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ package org.whispersystems.websocket.auth;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
|
||||
public interface WebSocketAuthenticator<T extends Principal> {
|
||||
|
||||
@@ -20,5 +20,5 @@ public interface WebSocketAuthenticator<T extends Principal> {
|
||||
*
|
||||
* @throws InvalidCredentialsException if credentials were provided, but could not be authenticated
|
||||
*/
|
||||
Optional<T> authenticate(UpgradeRequest request) throws InvalidCredentialsException;
|
||||
Optional<T> authenticate(JettyServerUpgradeRequest request) throws InvalidCredentialsException;
|
||||
}
|
||||
|
||||
+9
-8
@@ -5,22 +5,23 @@
|
||||
package org.whispersystems.websocket.messages;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public interface WebSocketMessageFactory {
|
||||
|
||||
public WebSocketMessage parseMessage(byte[] serialized, int offset, int len)
|
||||
WebSocketMessage parseMessage(ByteBuffer serialized)
|
||||
throws InvalidMessageException;
|
||||
|
||||
public WebSocketMessage createRequest(Optional<Long> requestId,
|
||||
String verb, String path,
|
||||
List<String> headers,
|
||||
Optional<byte[]> body);
|
||||
WebSocketMessage createRequest(Optional<Long> requestId,
|
||||
String verb, String path,
|
||||
List<String> headers,
|
||||
Optional<byte[]> body);
|
||||
|
||||
public WebSocketMessage createResponse(long requestId, int status, String message,
|
||||
List<String> headers,
|
||||
Optional<byte[]> body);
|
||||
WebSocketMessage createResponse(long requestId, int status, String message,
|
||||
List<String> headers,
|
||||
Optional<byte[]> body);
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -10,14 +10,15 @@ import org.whispersystems.websocket.messages.InvalidMessageException;
|
||||
import org.whispersystems.websocket.messages.WebSocketMessage;
|
||||
import org.whispersystems.websocket.messages.WebSocketRequestMessage;
|
||||
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ProtobufWebSocketMessage implements WebSocketMessage {
|
||||
|
||||
private final SubProtocol.WebSocketMessage message;
|
||||
|
||||
ProtobufWebSocketMessage(byte[] buffer, int offset, int length) throws InvalidMessageException {
|
||||
ProtobufWebSocketMessage(ByteBuffer buffer) throws InvalidMessageException {
|
||||
try {
|
||||
this.message = SubProtocol.WebSocketMessage.parseFrom(ByteString.copyFrom(buffer, offset, length));
|
||||
this.message = SubProtocol.WebSocketMessage.parseFrom(ByteString.copyFrom(buffer));
|
||||
|
||||
if (getType() == Type.REQUEST_MESSAGE) {
|
||||
if (!message.getRequest().hasVerb() || !message.getRequest().hasPath()) {
|
||||
|
||||
+3
-2
@@ -9,16 +9,17 @@ import org.whispersystems.websocket.messages.InvalidMessageException;
|
||||
import org.whispersystems.websocket.messages.WebSocketMessage;
|
||||
import org.whispersystems.websocket.messages.WebSocketMessageFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ProtobufWebSocketMessageFactory implements WebSocketMessageFactory {
|
||||
|
||||
@Override
|
||||
public WebSocketMessage parseMessage(byte[] serialized, int offset, int len)
|
||||
public WebSocketMessage parseMessage(ByteBuffer serialized)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
return new ProtobufWebSocketMessage(serialized, offset, len);
|
||||
return new ProtobufWebSocketMessage(serialized);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+7
-27
@@ -19,17 +19,15 @@ import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import javax.security.auth.Subject;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.websocket.auth.InvalidCredentialsException;
|
||||
import org.whispersystems.websocket.auth.AuthenticatedWebSocketUpgradeFilter;
|
||||
import org.whispersystems.websocket.auth.InvalidCredentialsException;
|
||||
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
|
||||
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||
|
||||
public class WebSocketResourceProviderFactoryTest {
|
||||
@@ -60,8 +58,7 @@ public class WebSocketResourceProviderFactoryTest {
|
||||
when(authenticator.authenticate(eq(request))).thenThrow(new InvalidCredentialsException());
|
||||
when(environment.jersey()).thenReturn(jerseyEnvironment);
|
||||
|
||||
WebSocketResourceProviderFactory<?> factory = new WebSocketResourceProviderFactory<>(environment, Account.class,
|
||||
mock(WebSocketConfiguration.class), REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
WebSocketResourceProviderFactory<?> factory = new WebSocketResourceProviderFactory<>(environment, Account.class, REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
Object connection = factory.createWebSocket(request, response);
|
||||
|
||||
assertNull(connection);
|
||||
@@ -80,16 +77,15 @@ public class WebSocketResourceProviderFactoryTest {
|
||||
final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);
|
||||
when(httpServletRequest.getAttribute(REMOTE_ADDRESS_PROPERTY_NAME)).thenReturn("127.0.0.1");
|
||||
when(request.getHttpServletRequest()).thenReturn(httpServletRequest);
|
||||
|
||||
WebSocketResourceProviderFactory<?> factory = new WebSocketResourceProviderFactory<>(environment, Account.class,
|
||||
mock(WebSocketConfiguration.class), REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
Object connection = factory.createWebSocket(request, response);
|
||||
|
||||
assertNotNull(connection);
|
||||
verifyNoMoreInteractions(response);
|
||||
verify(authenticator).authenticate(eq(request));
|
||||
|
||||
((WebSocketResourceProvider<?>) connection).onWebSocketConnect(mock(Session.class));
|
||||
((WebSocketResourceProvider<?>) connection).onWebSocketOpen(mock(Session.class));
|
||||
|
||||
assertNotNull(((WebSocketResourceProvider<?>) connection).getContext().getAuthenticated());
|
||||
assertEquals(((WebSocketResourceProvider<?>) connection).getContext().getAuthenticated(), account);
|
||||
@@ -103,7 +99,6 @@ public class WebSocketResourceProviderFactoryTest {
|
||||
|
||||
WebSocketResourceProviderFactory<Account> factory = new WebSocketResourceProviderFactory<>(environment,
|
||||
Account.class,
|
||||
mock(WebSocketConfiguration.class),
|
||||
REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
Object connection = factory.createWebSocket(request, response);
|
||||
|
||||
@@ -112,20 +107,6 @@ public class WebSocketResourceProviderFactoryTest {
|
||||
verify(authenticator).authenticate(eq(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigure() {
|
||||
JettyWebSocketServletFactory servletFactory = mock(JettyWebSocketServletFactory.class);
|
||||
when(environment.jersey()).thenReturn(jerseyEnvironment);
|
||||
|
||||
WebSocketResourceProviderFactory<Account> factory = new WebSocketResourceProviderFactory<>(environment,
|
||||
Account.class,
|
||||
mock(WebSocketConfiguration.class),
|
||||
REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
factory.configure(servletFactory);
|
||||
|
||||
verify(servletFactory).setCreator(eq(factory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAuthenticatedWebSocketUpgradeFilter() throws InvalidCredentialsException {
|
||||
final Account account = new Account();
|
||||
@@ -137,12 +118,11 @@ public class WebSocketResourceProviderFactoryTest {
|
||||
final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);
|
||||
when(httpServletRequest.getAttribute(REMOTE_ADDRESS_PROPERTY_NAME)).thenReturn("127.0.0.1");
|
||||
when(request.getHttpServletRequest()).thenReturn(httpServletRequest);
|
||||
|
||||
final AuthenticatedWebSocketUpgradeFilter<Account> filter = mock(AuthenticatedWebSocketUpgradeFilter.class);
|
||||
when(environment.getAuthenticatedWebSocketUpgradeFilter()).thenReturn(filter);
|
||||
|
||||
final WebSocketResourceProviderFactory<?> factory = new WebSocketResourceProviderFactory<>(environment, Account.class,
|
||||
mock(WebSocketConfiguration.class), REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
REMOTE_ADDRESS_PROPERTY_NAME);
|
||||
assertNotNull(factory.createWebSocket(request, response));
|
||||
|
||||
verify(filter).handleAuthentication(reusableAuth, request, response);
|
||||
|
||||
+43
-72
@@ -47,11 +47,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.eclipse.jetty.websocket.api.CloseStatus;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.glassfish.jersey.server.ApplicationHandler;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.ContainerResponse;
|
||||
@@ -92,11 +90,10 @@ class WebSocketResourceProviderTest {
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
verify(session, never()).close(anyInt(), anyString());
|
||||
verify(session, never()).close(anyInt(), anyString(), any(Callback.class));
|
||||
verify(session, never()).close();
|
||||
verify(session, never()).close(any(CloseStatus.class));
|
||||
|
||||
ArgumentCaptor<WebSocketSessionContext> contextArgumentCaptor = ArgumentCaptor.forClass(
|
||||
WebSocketSessionContext.class);
|
||||
@@ -114,11 +111,9 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
ContainerResponse response = mock(ContainerResponse.class);
|
||||
when(response.getStatus()).thenReturn(200);
|
||||
@@ -148,16 +143,15 @@ class WebSocketResourceProviderTest {
|
||||
return CompletableFuture.completedFuture(response);
|
||||
});
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
verify(session, never()).close(anyInt(), anyString());
|
||||
verify(session, never()).close(anyInt(), anyString(), any(Callback.class));
|
||||
verify(session, never()).close();
|
||||
verify(session, never()).close(any(CloseStatus.class));
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/bar",
|
||||
new LinkedList<>(), Optional.of("hello world!".getBytes())).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ContainerRequest> requestCaptor = ArgumentCaptor.forClass(ContainerRequest.class);
|
||||
ArgumentCaptor<ByteBuffer> responseCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
@@ -171,7 +165,7 @@ class WebSocketResourceProviderTest {
|
||||
assertThat(bundledRequest.getPath(false)).isEqualTo("bar");
|
||||
|
||||
verify(requestLog).log(eq("127.0.0.1"), eq(bundledRequest), eq(response));
|
||||
verify(remoteEndpoint).sendBytes(responseCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketMessage responseMessageContainer = SubProtocol.WebSocketMessage.parseFrom(
|
||||
responseCaptor.getValue().array());
|
||||
@@ -191,25 +185,22 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
when(applicationHandler.apply(any(ContainerRequest.class), any(OutputStream.class))).thenReturn(
|
||||
CompletableFuture.failedFuture(new IllegalStateException("foo")));
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
verify(session, never()).close(anyInt(), anyString());
|
||||
verify(session, never()).close(anyInt(), anyString(), any(Callback.class));
|
||||
verify(session, never()).close();
|
||||
verify(session, never()).close(any(CloseStatus.class));
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/bar",
|
||||
new LinkedList<>(), Optional.of("hello world!".getBytes())).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ContainerRequest> requestCaptor = ArgumentCaptor.forClass(ContainerRequest.class);
|
||||
|
||||
@@ -223,7 +214,7 @@ class WebSocketResourceProviderTest {
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketMessage responseMessageContainer = SubProtocol.WebSocketMessage.parseFrom(
|
||||
responseCaptor.getValue().array());
|
||||
@@ -247,22 +238,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/hello",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -287,22 +276,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET",
|
||||
"/v1/test/doesntexist", new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -327,22 +314,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/world",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -367,22 +352,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/world",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -406,22 +389,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/optional",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -446,22 +427,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/optional",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -486,23 +465,21 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "PUT",
|
||||
"/v1/test/some/testparam", List.of("Content-Type: application/json"),
|
||||
Optional.of(new ObjectMapper().writeValueAsBytes(new TestResource.TestEntity("mykey", 1001)))).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -527,23 +504,21 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "PUT",
|
||||
"/v1/test/some/testparam", List.of("Content-Type: application/json"),
|
||||
Optional.of(new ObjectMapper().writeValueAsBytes(new TestResource.TestEntity("mykey", 5)))).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -569,22 +544,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET",
|
||||
"/v1/test/exception/map", List.of("Content-Type: application/json"), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
@@ -609,22 +582,20 @@ class WebSocketResourceProviderTest {
|
||||
new ProtobufWebSocketMessageFactory(), Optional.empty(), Duration.ofMillis(30000));
|
||||
|
||||
Session session = mock(Session.class);
|
||||
RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
|
||||
UpgradeRequest request = mock(UpgradeRequest.class);
|
||||
|
||||
when(session.getUpgradeRequest()).thenReturn(request);
|
||||
when(session.getRemote()).thenReturn(remoteEndpoint);
|
||||
|
||||
provider.onWebSocketConnect(session);
|
||||
provider.onWebSocketOpen(session);
|
||||
|
||||
byte[] message = new ProtobufWebSocketMessageFactory().createRequest(Optional.of(111L), "GET", "/v1/test/keepalive",
|
||||
new LinkedList<>(), Optional.empty()).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(message, 0, message.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(message), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> requestCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint).sendBytes(requestCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session).sendBinary(requestCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketRequestMessage requestMessage = getRequest(requestCaptor);
|
||||
assertThat(requestMessage.getVerb()).isEqualTo("GET");
|
||||
@@ -634,11 +605,11 @@ class WebSocketResourceProviderTest {
|
||||
byte[] clientResponse = new ProtobufWebSocketMessageFactory().createResponse(requestMessage.getId(), 200, "OK",
|
||||
new LinkedList<>(), Optional.of("my response".getBytes())).toByteArray();
|
||||
|
||||
provider.onWebSocketBinary(clientResponse, 0, clientResponse.length);
|
||||
provider.onWebSocketBinary(ByteBuffer.wrap(clientResponse), Callback.NOOP);
|
||||
|
||||
ArgumentCaptor<ByteBuffer> responseBytesCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
|
||||
|
||||
verify(remoteEndpoint, times(2)).sendBytes(responseBytesCaptor.capture(), any(WriteCallback.class));
|
||||
verify(session, times(2)).sendBinary(responseBytesCaptor.capture(), any(Callback.class));
|
||||
|
||||
SubProtocol.WebSocketResponseMessage response = getResponse(responseBytesCaptor);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user