Skip to content

Commit 1e26d55

Browse files
committed
Add X-Signal-Timestamp response filter
1 parent b05957b commit 1e26d55

File tree

4 files changed

+128
-13
lines changed

4 files changed

+128
-13
lines changed

src/main/java/org/signal/storageservice/StorageService.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55

66
package org.signal.storageservice;
77

8-
import static com.codahale.metrics.MetricRegistry.name;
9-
10-
import com.codahale.metrics.SharedMetricRegistries;
118
import com.fasterxml.jackson.annotation.JsonAutoDetect;
129
import com.fasterxml.jackson.annotation.PropertyAccessor;
1310
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -23,9 +20,6 @@
2320
import io.dropwizard.core.Application;
2421
import io.dropwizard.core.setup.Bootstrap;
2522
import io.dropwizard.core.setup.Environment;
26-
import io.micrometer.core.instrument.Metrics;
27-
import io.micrometer.core.instrument.Tags;
28-
import io.micrometer.datadog.DatadogMeterRegistry;
2923
import java.time.Clock;
3024
import java.util.Set;
3125
import org.signal.libsignal.zkgroup.ServerSecretParams;
@@ -41,14 +35,9 @@
4135
import org.signal.storageservice.controllers.HealthCheckController;
4236
import org.signal.storageservice.controllers.ReadinessController;
4337
import org.signal.storageservice.controllers.StorageController;
44-
import org.signal.storageservice.metrics.CpuUsageGauge;
45-
import org.signal.storageservice.metrics.FileDescriptorGauge;
46-
import org.signal.storageservice.metrics.FreeMemoryGauge;
38+
import org.signal.storageservice.filters.TimestampResponseFilter;
4739
import org.signal.storageservice.metrics.MetricsApplicationEventListener;
4840
import org.signal.storageservice.metrics.MetricsUtil;
49-
import org.signal.storageservice.metrics.NetworkReceivedGauge;
50-
import org.signal.storageservice.metrics.NetworkSentGauge;
51-
import org.signal.storageservice.metrics.StorageMetrics;
5241
import org.signal.storageservice.providers.CompletionExceptionMapper;
5342
import org.signal.storageservice.providers.InvalidProtocolBufferExceptionMapper;
5443
import org.signal.storageservice.providers.ProtocolBufferMessageBodyProvider;
@@ -58,7 +47,6 @@
5847
import org.signal.storageservice.storage.GroupsManager;
5948
import org.signal.storageservice.storage.StorageManager;
6049
import org.signal.storageservice.util.UncaughtExceptionHandler;
61-
import org.signal.storageservice.util.HostSupplier;
6250
import org.signal.storageservice.util.logging.LoggingUnhandledExceptionMapper;
6351

6452
public class StorageService extends Application<StorageServiceConfiguration> {
@@ -105,6 +93,8 @@ public void run(StorageServiceConfiguration config, Environment environment) thr
10593
environment.jersey().register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(User.class, userAuthFilter, GroupUser.class, groupUserAuthFilter)));
10694
environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(User.class, GroupUser.class)));
10795

96+
environment.jersey().register(new TimestampResponseFilter(Clock.systemUTC()));
97+
10898
environment.jersey().register(new HealthCheckController());
10999
environment.jersey().register(new ReadinessController(bigtableDataClient,
110100
Set.of(config.getBigTableConfiguration().getGroupsTableId(),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.signal.storageservice.filters;
7+
8+
import java.io.IOException;
9+
import java.time.Clock;
10+
import javax.servlet.Filter;
11+
import javax.servlet.FilterChain;
12+
import javax.servlet.ServletException;
13+
import javax.servlet.ServletRequest;
14+
import javax.servlet.ServletResponse;
15+
import javax.servlet.http.HttpServletResponse;
16+
import javax.ws.rs.container.ContainerRequestContext;
17+
import javax.ws.rs.container.ContainerResponseContext;
18+
import javax.ws.rs.container.ContainerResponseFilter;
19+
import org.signal.storageservice.util.HeaderUtils;
20+
21+
/**
22+
* Injects a timestamp header into all outbound responses.
23+
*/
24+
public class TimestampResponseFilter implements Filter, ContainerResponseFilter {
25+
26+
private final Clock clock;
27+
28+
public TimestampResponseFilter(final Clock clock) {
29+
this.clock = clock;
30+
}
31+
32+
@Override
33+
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
34+
throws ServletException, IOException {
35+
36+
if (response instanceof HttpServletResponse httpServletResponse) {
37+
httpServletResponse.setHeader(HeaderUtils.TIMESTAMP_HEADER, String.valueOf(clock.millis()));
38+
}
39+
40+
chain.doFilter(request, response);
41+
}
42+
43+
@Override
44+
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
45+
// not using add() - it's ok to overwrite any existing header, and we don't want a multi-value
46+
responseContext.getHeaders().putSingle(HeaderUtils.TIMESTAMP_HEADER, String.valueOf(clock.millis()));
47+
}
48+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2025 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.signal.storageservice.util;
7+
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
public final class HeaderUtils {
12+
13+
private static final Logger logger = LoggerFactory.getLogger(HeaderUtils.class);
14+
15+
public static final String TIMESTAMP_HEADER = "X-Signal-Timestamp";
16+
17+
private HeaderUtils() {
18+
// utility class
19+
}
20+
21+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.signal.storageservice.filters;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
10+
import static org.mockito.ArgumentMatchers.eq;
11+
import static org.mockito.Mockito.mock;
12+
import static org.mockito.Mockito.verify;
13+
import static org.mockito.Mockito.when;
14+
15+
import java.time.Clock;
16+
import java.time.Instant;
17+
import java.time.ZoneId;
18+
import javax.servlet.FilterChain;
19+
import javax.servlet.http.HttpServletRequest;
20+
import javax.servlet.http.HttpServletResponse;
21+
import javax.ws.rs.container.ContainerRequestContext;
22+
import javax.ws.rs.container.ContainerResponseContext;
23+
import javax.ws.rs.core.MultivaluedMap;
24+
import org.junit.jupiter.api.Test;
25+
import org.signal.storageservice.util.HeaderUtils;
26+
27+
class TimestampResponseFilterTest {
28+
29+
private static final long EPOCH_MILLIS = 1738182156000L;
30+
31+
private static final Clock CLOCK = Clock.fixed(Instant.ofEpochMilli(EPOCH_MILLIS), ZoneId.systemDefault());
32+
33+
@Test
34+
void testJerseyFilter() {
35+
final ContainerRequestContext requestContext = mock(ContainerRequestContext.class);
36+
final ContainerResponseContext responseContext = mock(ContainerResponseContext.class);
37+
final MultivaluedMap<String, Object> headers = org.glassfish.jersey.message.internal.HeaderUtils.createOutbound();
38+
when(responseContext.getHeaders()).thenReturn(headers);
39+
40+
new TimestampResponseFilter(CLOCK).filter(requestContext, responseContext);
41+
42+
assertTrue(headers.containsKey(HeaderUtils.TIMESTAMP_HEADER));
43+
assertEquals(1, headers.get(HeaderUtils.TIMESTAMP_HEADER).size());
44+
assertEquals(String.valueOf(EPOCH_MILLIS), headers.get(HeaderUtils.TIMESTAMP_HEADER).get(0));
45+
}
46+
47+
@Test
48+
void testServletFilter() throws Exception {
49+
final HttpServletRequest request = mock(HttpServletRequest.class);
50+
final HttpServletResponse response = mock(HttpServletResponse.class);
51+
52+
new TimestampResponseFilter(CLOCK).doFilter(request, response, mock(FilterChain.class));
53+
54+
verify(response).setHeader(eq(HeaderUtils.TIMESTAMP_HEADER), eq(String.valueOf(EPOCH_MILLIS)));
55+
}
56+
}

0 commit comments

Comments
 (0)