mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 16:31:52 +01:00
committed by
ravi-signal
parent
bb94975d74
commit
faa74469ea
@@ -37,12 +37,15 @@ import io.netty.resolver.ResolvedAddressTypes;
|
||||
import io.netty.resolver.dns.DnsNameResolver;
|
||||
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.ServletRegistration;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
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;
|
||||
@@ -57,9 +60,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;
|
||||
@@ -133,7 +136,6 @@ 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;
|
||||
@@ -182,7 +184,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.MetricsHttpEventHandler;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsHttpChannelListener;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.MicrometerAwsSdkMetricPublisher;
|
||||
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
|
||||
@@ -595,6 +597,7 @@ 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 ManagedNioEventLoopGroup dnsResolutionEventLoopGroup = new ManagedNioEventLoopGroup();
|
||||
final DnsNameResolver cloudflareDnsResolver = new DnsNameResolverBuilder(dnsResolutionEventLoopGroup.next())
|
||||
@@ -910,6 +913,17 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
environment.lifecycle().manage(dnsResolutionEventLoopGroup);
|
||||
environment.lifecycle().manage(exposedGrpcServer);
|
||||
|
||||
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(),
|
||||
@@ -926,7 +940,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
final String websocketServletPath = "/v1/websocket/";
|
||||
final String provisioningWebsocketServletPath = "/v1/websocket/provisioning/";
|
||||
|
||||
MetricsHttpEventHandler.configure(environment, Metrics.globalRegistry, clientReleaseManager, Set.of(websocketServletPath, provisioningWebsocketServletPath, "/health-check"));
|
||||
final MetricsHttpChannelListener metricsHttpChannelListener = new MetricsHttpChannelListener(clientReleaseManager,
|
||||
Set.of(websocketServletPath, provisioningWebsocketServletPath, "/health-check"));
|
||||
metricsHttpChannelListener.configure(environment);
|
||||
final MessageMetrics messageMetrics = new MessageMetrics();
|
||||
final BackupMetrics backupMetrics = new BackupMetrics();
|
||||
|
||||
@@ -1116,35 +1132,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, RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
webSocketEnvironment, AuthenticatedDevice.class, config.getWebSocketConfiguration(),
|
||||
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
WebSocketResourceProviderFactory<AuthenticatedDevice> provisioningServlet = new WebSocketResourceProviderFactory<>(
|
||||
provisioningEnvironment, AuthenticatedDevice.class, RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
provisioningEnvironment, AuthenticatedDevice.class, config.getWebSocketConfiguration(),
|
||||
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(),
|
||||
(servletContext, container) -> {
|
||||
container.addMapping(websocketServletPath, webSocketServlet);
|
||||
container.addMapping(provisioningWebsocketServletPath, provisioningServlet);
|
||||
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet);
|
||||
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
|
||||
|
||||
PriorityFilter.ensureFilter(servletContext, new TimestampResponseFilter());
|
||||
PriorityFilter.ensureFilter(servletContext, new RemoteAddressFilter());
|
||||
PriorityFilter.ensureFilter(servletContext, remoteDeprecationFilter);
|
||||
websocket.addMapping(websocketServletPath);
|
||||
websocket.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);
|
||||
}
|
||||
});
|
||||
provisioning.addMapping(provisioningWebsocketServletPath);
|
||||
provisioning.setAsyncSupported(true);
|
||||
|
||||
environment.admin().addTask(new SetRequestLoggingEnabledTask());
|
||||
|
||||
}
|
||||
|
||||
private void registerExceptionMappers(Environment environment,
|
||||
|
||||
@@ -10,9 +10,10 @@ 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.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.websocket.auth.AuthenticatedWebSocketUpgradeFilter;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,10 @@ 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 {
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
/**
|
||||
* Delegates request events to a listener that captures and reports request-level metrics.
|
||||
*
|
||||
* @see MetricsHttpEventHandler
|
||||
* @see MetricsHttpChannelListener
|
||||
* @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 " + MetricsHttpEventHandler.class.getName() + " for HTTP traffic");
|
||||
throw new IllegalArgumentException("Use " + MetricsHttpChannelListener.class.getName() + " for HTTP traffic");
|
||||
}
|
||||
this.metricsRequestEventListener = new MetricsRequestEventListener(trafficSource, clientReleaseManager);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.Optional;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
|
||||
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.addAll(UserAgentTagUtil.getLibsignalAndPlatformTags(requestInfo.userAgent()));
|
||||
|
||||
final Optional<Tag> maybeClientVersionTag =
|
||||
UserAgentTagUtil.getClientVersionTag(requestInfo.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, UserAgentTagUtil.getPlatformTag(requestInfo.userAgent)))
|
||||
.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")
|
||||
);
|
||||
final String method = Optional.ofNullable(request.getMethod()).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, method, status, userAgent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,250 +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.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.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
|
||||
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.addAll(UserAgentTagUtil.getLibsignalAndPlatformTags(requestInfo.userAgent));
|
||||
|
||||
final Optional<Tag> maybeClientVersionTag =
|
||||
UserAgentTagUtil.getClientVersionTag(requestInfo.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, UserAgentTagUtil.getPlatformTag(requestInfo.userAgent)))
|
||||
.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"),
|
||||
Optional.ofNullable(request.getMethod()).orElse("unknown"),
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.eclipse.jetty.io.ArrayByteBufferPool;
|
||||
import org.glassfish.jersey.server.ContainerResponse;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEventListener;
|
||||
@@ -28,7 +29,7 @@ import org.whispersystems.websocket.WebSocketResourceProvider;
|
||||
|
||||
/**
|
||||
* Gathers and reports request-level metrics for WebSocket traffic only.
|
||||
* For HTTP traffic, use {@link MetricsHttpEventHandler}.
|
||||
* For HTTP traffic, use {@link MetricsHttpChannelListener}.
|
||||
*/
|
||||
public class MetricsRequestEventListener implements RequestEventListener {
|
||||
|
||||
@@ -63,7 +64,7 @@ public class MetricsRequestEventListener implements RequestEventListener {
|
||||
this(trafficSource, Metrics.globalRegistry, clientReleaseManager);
|
||||
|
||||
if (trafficSource == TrafficSource.HTTP) {
|
||||
logger.warn("Use {} for HTTP traffic", MetricsHttpEventHandler.class.getName());
|
||||
logger.warn("Use {} for HTTP traffic", MetricsHttpChannelListener.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.PathResourceFactory;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
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(new PathResourceFactory().newResource(keyStorePath), keyStoreType, keyStoreProvider,
|
||||
keyStore = CertificateUtils.getKeyStore(Resource.newResource(keyStorePath), keyStoreType, keyStoreProvider,
|
||||
keyStorePassword);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,14 @@ package org.whispersystems.textsecuregcm.websocket;
|
||||
import static org.whispersystems.textsecuregcm.util.HeaderUtils.basicCredentialsFromAuthHeader;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyServerUpgradeRequest;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import org.eclipse.jetty.websocket.api.UpgradeRequest;
|
||||
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 JettyServerUpgradeRequest request)
|
||||
public Optional<AuthenticatedDevice> authenticate(final UpgradeRequest request)
|
||||
throws InvalidCredentialsException {
|
||||
|
||||
@Nullable final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
Reference in New Issue
Block a user