From 830531bcb03326986e919f07fe2cf0cbb5338214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20R=C3=B6bke?= <4270+boosty@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:56:15 +0100 Subject: [PATCH] Set default User-Agent in HTTP client to "Graylog" (#21864) * Set default User-Agent in HTTP client to "Graylog" * Add changelog * Add our default user agent only if none is set This allows custom user agent strings on a per-request basis. * Update changelog wording --------- Co-authored-by: Bernd Ahlers --- changelog/unreleased/pr-21864.toml | 5 +++ .../graylog2/plugin/BaseConfiguration.java | 7 ++++ .../providers/OkHttpClientProvider.java | 41 +++++++++++++++---- .../providers/OkHttpClientProviderTest.java | 25 +++++++++-- .../ParameterizedHttpClientProviderTest.java | 4 +- misc/graylog.conf | 4 ++ 6 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 changelog/unreleased/pr-21864.toml diff --git a/changelog/unreleased/pr-21864.toml b/changelog/unreleased/pr-21864.toml new file mode 100644 index 0000000000..289c607e7f --- /dev/null +++ b/changelog/unreleased/pr-21864.toml @@ -0,0 +1,5 @@ +type = "c" +message = "Set default `User-Agent` header in HTTP client to `Graylog`." + +issues = [""] +pulls = ["21864"] diff --git a/graylog2-server/src/main/java/org/graylog2/plugin/BaseConfiguration.java b/graylog2-server/src/main/java/org/graylog2/plugin/BaseConfiguration.java index b080212461..7b8c8ba650 100644 --- a/graylog2-server/src/main/java/org/graylog2/plugin/BaseConfiguration.java +++ b/graylog2-server/src/main/java/org/graylog2/plugin/BaseConfiguration.java @@ -85,6 +85,9 @@ public abstract class BaseConfiguration extends PathConfiguration implements Com @Parameter("disable_native_system_stats_collector") private boolean disableNativeSystemStatsCollector = false; + @Parameter(value = "http_user_agent") + private String httpUserAgent = "Graylog"; + @Parameter(value = "http_proxy_uri") private URI httpProxyUri; @@ -179,6 +182,10 @@ public abstract class BaseConfiguration extends PathConfiguration implements Com return disableNativeSystemStatsCollector; } + public String getHttpUserAgent() { + return httpUserAgent; + } + public URI getHttpProxyUri() { return httpProxyUri; } diff --git a/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java b/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java index c11fdd1cb3..582bb9a1c8 100644 --- a/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java +++ b/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/OkHttpClientProvider.java @@ -18,9 +18,15 @@ package org.graylog2.shared.bindings.providers; import com.github.joschi.jadconfig.util.Duration; import com.google.common.base.Splitter; +import com.google.common.net.HttpHeaders; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; import okhttp3.Authenticator; import okhttp3.Challenge; import okhttp3.Credentials; +import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -31,12 +37,6 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; - -import jakarta.inject.Inject; -import jakarta.inject.Named; -import jakarta.inject.Provider; -import jakarta.inject.Singleton; - import java.io.IOException; import java.net.URI; import java.util.List; @@ -51,6 +51,7 @@ import static java.util.Objects.requireNonNull; /** * Provider for a configured {@link okhttp3.OkHttpClient}. * + * @see org.graylog2.plugin.BaseConfiguration#getHttpUserAgent() * @see org.graylog2.plugin.BaseConfiguration#getHttpConnectTimeout() * @see org.graylog2.plugin.BaseConfiguration#getHttpReadTimeout() * @see org.graylog2.plugin.BaseConfiguration#getHttpWriteTimeout() @@ -59,6 +60,29 @@ import static java.util.Objects.requireNonNull; @Singleton public class OkHttpClientProvider implements Provider { private static final Logger LOG = LoggerFactory.getLogger(OkHttpClientProvider.class); + + static class UserAgentInterceptor implements Interceptor { + private final String userAgent; + + public UserAgentInterceptor(String userAgent) { + this.userAgent = userAgent; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + // Add our default user agent, but only if none is set + if (originalRequest.header(HttpHeaders.USER_AGENT) == null) { + final var builder = originalRequest.newBuilder(); + final var newRequest = builder.header(HttpHeaders.USER_AGENT, userAgent).build(); + return chain.proceed(newRequest); + } else { + return chain.proceed(originalRequest); + } + } + } + + protected final String userAgent; protected final Duration connectTimeout; protected final Duration readTimeout; protected final Duration writeTimeout; @@ -67,12 +91,14 @@ public class OkHttpClientProvider implements Provider { private final ProxySelectorProvider proxySelectorProvider; @Inject - public OkHttpClientProvider(@Named("http_connect_timeout") Duration connectTimeout, + public OkHttpClientProvider(@Named("http_user_agent") String userAgent, + @Named("http_connect_timeout") Duration connectTimeout, @Named("http_read_timeout") Duration readTimeout, @Named("http_write_timeout") Duration writeTimeout, @Named("http_proxy_uri") @Nullable URI httpProxyUri, TrustManagerAndSocketFactoryProvider trustManagerAndSocketFactoryProvider, ProxySelectorProvider proxySelectorProvider) { + this.userAgent = requireNonNull(userAgent); this.connectTimeout = requireNonNull(connectTimeout); this.readTimeout = requireNonNull(readTimeout); this.writeTimeout = requireNonNull(writeTimeout); @@ -85,6 +111,7 @@ public class OkHttpClientProvider implements Provider { public OkHttpClient get() { final OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() .retryOnConnectionFailure(true) + .addInterceptor(new UserAgentInterceptor(userAgent)) .connectTimeout(connectTimeout.getQuantity(), connectTimeout.getUnit()) .writeTimeout(writeTimeout.getQuantity(), writeTimeout.getUnit()) .readTimeout(readTimeout.getQuantity(), readTimeout.getUnit()); diff --git a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java index a233720c3f..c5f4b8c2ba 100644 --- a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java +++ b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/OkHttpClientProviderTest.java @@ -291,11 +291,11 @@ public class OkHttpClientProviderTest { final ProxySelectorProvider proxyProvider = new ProxySelectorProvider(server.url("/").uri(), null); ProxySelectorProvider spyProxyProvider = Mockito.spy(proxyProvider); final OkHttpClientProvider provider = new OkHttpClientProvider( + "GraylogTest", Duration.milliseconds(100L), Duration.milliseconds(100L), Duration.milliseconds(100L), - server.url("/").uri(), - null, spyProxyProvider); + server.url("/").uri(), null, spyProxyProvider); OkHttpClientProvider spyClientProvider = Mockito.spy(provider); @@ -312,6 +312,23 @@ public class OkHttpClientProviderTest { .matches(proxy -> proxy.equals(testProxy)); } + @Test + public void testDefaultUserAgent() throws IOException, InterruptedException { + server.enqueue(successfulMockResponse()); + client(server.url("/").uri()).newCall(request()).execute(); + final RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.USER_AGENT)).isEqualTo("GraylogTest"); + } + + @Test + public void testCustomUserAgent() throws IOException, InterruptedException { + server.enqueue(successfulMockResponse()); + final var request = request().newBuilder().header(HttpHeaders.USER_AGENT, "foo").build(); + client(server.url("/").uri()).newCall(request).execute(); + final RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader(HttpHeaders.USER_AGENT)).isEqualTo("foo"); + } + private MockResponse successfulMockResponse() { return new MockResponse().setResponseCode(200).setBody("Test"); } @@ -334,11 +351,11 @@ public class OkHttpClientProviderTest { private OkHttpClient client(URI proxyURI) { final OkHttpClientProvider provider = new OkHttpClientProvider( + "GraylogTest", Duration.milliseconds(100L), Duration.milliseconds(100L), Duration.milliseconds(100L), - proxyURI, - null, new ProxySelectorProvider(proxyURI, null)); + proxyURI, null, new ProxySelectorProvider(proxyURI, null)); return provider.get(); } diff --git a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java index 9dea0422ed..13d035eb29 100644 --- a/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java +++ b/graylog2-server/src/test/java/org/graylog2/shared/bindings/providers/ParameterizedHttpClientProviderTest.java @@ -142,11 +142,11 @@ public class ParameterizedHttpClientProviderTest { private OkHttpClientProvider client(URI proxyURI) { final OkHttpClientProvider provider = new OkHttpClientProvider( + "GraylogTest", Duration.milliseconds(500L), Duration.milliseconds(500L), Duration.milliseconds(500L), - proxyURI, - null, null); + proxyURI, null, null); return provider; } diff --git a/misc/graylog.conf b/misc/graylog.conf index 2d874a95ff..c9f80c453b 100644 --- a/misc/graylog.conf +++ b/misc/graylog.conf @@ -624,6 +624,10 @@ mongodb_max_connections = 1000 # This should define the fully qualified base url to your web interface exactly the same way as it is accessed by your users. #transport_email_web_interface_url = https://graylog.example.com +# The User-Agent header for outgoing HTTP connections. +# Default: Graylog +#http_user_agent = Graylog + # The default connect timeout for outgoing HTTP connections. # Values must be a positive duration (and between 1 and 2147483647 when converted to milliseconds). # Default: 5s