From b98f3353d370acdd20ce907c4d3eb232861badd2 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Portier Date: Tue, 28 Nov 2023 14:55:52 +0100 Subject: [PATCH] feat: Send SDK 'User-Agent' header --- client/resources/version.properties | 4 ++ .../src/main/com/sinch/sdk/SinchClient.java | 58 +++++++++++++++++-- .../com/sinch/sdk/http/HttpClientApache.java | 20 +++++++ .../java/com/sinch/sdk/SinchClientTestIT.java | 57 ++++++++++++++++++ .../adapters/apache/HttpClientTestIT.java | 46 +++++++++++++++ .../com/sinch/sdk/core/http/HttpClient.java | 8 +++ pom.xml | 12 ++++ 7 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 client/resources/version.properties create mode 100644 client/src/test/java/com/sinch/sdk/SinchClientTestIT.java diff --git a/client/resources/version.properties b/client/resources/version.properties new file mode 100644 index 00000000..f452de19 --- /dev/null +++ b/client/resources/version.properties @@ -0,0 +1,4 @@ +project.version=${project.version} +project.name=${project.name} +project.auxiliary_flag= +project.java.version=${maven.compiler.source} diff --git a/client/src/main/com/sinch/sdk/SinchClient.java b/client/src/main/com/sinch/sdk/SinchClient.java index c4d92875..55ccf43f 100644 --- a/client/src/main/com/sinch/sdk/SinchClient.java +++ b/client/src/main/com/sinch/sdk/SinchClient.java @@ -4,6 +4,7 @@ import com.sinch.sdk.auth.adapters.BasicAuthManager; import com.sinch.sdk.auth.adapters.BearerAuthManager; import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.numbers.NumbersService; import com.sinch.sdk.domains.sms.SMSService; import com.sinch.sdk.http.HttpClientApache; @@ -12,6 +13,8 @@ import java.io.IOException; import java.io.InputStream; import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collection; import java.util.Map; import java.util.Objects; import java.util.Properties; @@ -22,14 +25,28 @@ /** Sinch Sdk Client implementation */ public class SinchClient { + private static final String DEFAULT_PROPERTIES_FILE_NAME = "/config-default.properties"; + private static final String VERSION_PROPERTIES_FILE_NAME = "/version.properties"; + private static final String OAUTH_URL_KEY = "oauth-url"; private static final String NUMBERS_SERVER_KEY = "numbers-server"; private static final String SMS_REGION_KEY = "sms-region"; private static final String SMS_SERVER_KEY = "sms-server"; + private static final String PROJECT_NAME_KEY = "project.name"; + private static final String PROJECT_VERSION_KEY = "project.version"; + private static final String PROJECT_JAVA_VERSION_KEY = "project.java.version"; + private static final String PROJECT_AUXILIARY_FLAG = "project.auxiliary_flag"; + + // sinch-sdk/{sdk_version} ({language}/{language_version}; {implementation_type}; + // {auxiliary_flag}) + private static final String SDK_USER_AGENT_HEADER = "User-Agent"; + private static final String SDK_USER_AGENT_FORMAT = "sinch-sdk/%s (%s/%s; %s; %s)"; private static final Logger LOGGER = Logger.getLogger(SinchClient.class.getName()); private final Configuration configuration; + private final Properties versionProperties; + private NumbersService numbers; private SMSService sms; @@ -46,7 +63,7 @@ public SinchClient(Configuration configuration) { Configuration.Builder builder = Configuration.builder(configuration); - Properties props = handleDefaultConfigurationFile(); + Properties props = handlePropertiesFile(DEFAULT_PROPERTIES_FILE_NAME); if (null == configuration.getOAuthUrl() && props.containsKey(OAUTH_URL_KEY)) { builder.setOAuthUrl(props.getProperty(OAUTH_URL_KEY)); } @@ -63,7 +80,13 @@ public SinchClient(Configuration configuration) { checkConfiguration(newConfiguration); this.configuration = newConfiguration; - LOGGER.fine("SinchClient started with projectId='" + configuration.getProjectId() + "'"); + versionProperties = handlePropertiesFile(VERSION_PROPERTIES_FILE_NAME); + LOGGER.fine( + String.format( + "%s (%s) started with projectId '%s'", + versionProperties.getProperty(PROJECT_NAME_KEY), + versionProperties.getProperty(PROJECT_VERSION_KEY), + configuration.getProjectId())); } /** @@ -130,10 +153,10 @@ private SMSService smsInit() { return new com.sinch.sdk.domains.sms.adapters.SMSService(getConfiguration(), getHttpClient()); } - private Properties handleDefaultConfigurationFile() { + private Properties handlePropertiesFile(String fileName) { Properties prop = new Properties(); - try (InputStream is = this.getClass().getResourceAsStream("/config-default.properties")) { + try (InputStream is = this.getClass().getResourceAsStream(fileName)) { prop.load(is); } catch (IOException e) { // NOOP @@ -159,8 +182,35 @@ private HttpClientApache getHttpClient() { // Avoid multiple and costly http client creation and reuse it for authManager bearerAuthManager.setHttpClient(this.httpClient); + // set SDK User-Agent + String userAgent = formatSdkUserAgentHeader(versionProperties); + this.httpClient.setRequestHeaders( + Stream.of(new String[][] {{SDK_USER_AGENT_HEADER, userAgent}}) + .collect(Collectors.toMap(data -> data[0], data -> data[1]))); + LOGGER.fine("HTTP client loaded"); } return this.httpClient; } + + private String formatSdkUserAgentHeader(Properties versionProperties) { + return String.format( + SDK_USER_AGENT_FORMAT, + versionProperties.get(PROJECT_VERSION_KEY), + "Java", + versionProperties.get(PROJECT_JAVA_VERSION_KEY), + "Apache", + formatAuxiliaryFlag((String) versionProperties.get(PROJECT_AUXILIARY_FLAG))); + } + + private String formatAuxiliaryFlag(String auxiliaryFlag) { + + Collection values = + Arrays.asList(System.getProperty("java.vendor"), System.getProperty("java.version")); + + if (!StringUtil.isEmpty(auxiliaryFlag)) { + values.add(auxiliaryFlag); + } + return String.join(",", values); + } } diff --git a/client/src/main/com/sinch/sdk/http/HttpClientApache.java b/client/src/main/com/sinch/sdk/http/HttpClientApache.java index 588ff3f4..cda3d1f7 100644 --- a/client/src/main/com/sinch/sdk/http/HttpClientApache.java +++ b/client/src/main/com/sinch/sdk/http/HttpClientApache.java @@ -26,6 +26,8 @@ public class HttpClientApache implements com.sinch.sdk.core.http.HttpClient { private static final Logger LOGGER = Logger.getLogger(HttpClientApache.class.getName()); private static final String AUTHORIZATION_HEADER_KEYWORD = "Authorization"; private final Map authManagers; + + private Map headersToBeAdded; private CloseableHttpClient client; public HttpClientApache(Map authManagers) { @@ -33,6 +35,10 @@ public HttpClientApache(Map authManagers) { this.authManagers = authManagers; } + public void setRequestHeaders(Map headers) { + this.headersToBeAdded = headers; + } + private static HttpResponse processResponse(ClassicHttpResponse response) throws IOException { int statusCode = response.getCode(); @@ -98,6 +104,9 @@ public HttpResponse invokeAPI(ServerConfiguration serverConfiguration, HttpReque addCollectionHeader(requestBuilder, "Content-Type", contentType); addCollectionHeader(requestBuilder, "Accept", accept); + addHeaders(requestBuilder, headerParams); + addHeaders(requestBuilder, headersToBeAdded); + addAuth(requestBuilder, authNames); ClassicHttpRequest request = requestBuilder.build(); @@ -169,6 +178,17 @@ private void addCollectionHeader( } } + private void addHeaders(ClassicRequestBuilder requestBuilder, Map headers) { + + if (null == headers) { + return; + } + headers + .entrySet() + .iterator() + .forEachRemaining(f -> requestBuilder.setHeader(f.getKey(), f.getValue())); + } + private void addAuth(ClassicRequestBuilder requestBuilder, Collection values) { if (null == values || values.isEmpty()) { return; diff --git a/client/src/test/java/com/sinch/sdk/SinchClientTestIT.java b/client/src/test/java/com/sinch/sdk/SinchClientTestIT.java new file mode 100644 index 00000000..eaa16672 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/SinchClientTestIT.java @@ -0,0 +1,57 @@ +package com.sinch.sdk; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.models.Configuration; +import java.io.IOException; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@TestWithResources +class SinchClientTestIT extends BaseTest { + static MockWebServer mockBackEnd; + String serverUrl = String.format("http://localhost:%s", mockBackEnd.getPort()); + + Configuration configuration = + Configuration.builder() + .setKeyId("foo") + .setKeySecret("foo") + .setProjectId("foo") + .setNumbersUrl(serverUrl) + .build(); + + SinchClient sinchClient = new SinchClient(configuration); + + @BeforeAll + static void classSetUp() throws IOException { + mockBackEnd = new MockWebServer(); + mockBackEnd.start(); + } + + @AfterAll + static void tearDown() throws IOException { + mockBackEnd.shutdown(); + } + + @Test + void sdkUserAgent() throws InterruptedException { + + mockBackEnd.enqueue( + new MockResponse().setBody("foo").addHeader("Content-Type", "application/json")); + + try { + sinchClient.numbers().available().checkAvailability("foo"); + } catch (ApiException ae) { + // noop + } + RecordedRequest recordedRequest = mockBackEnd.takeRequest(); + String header = recordedRequest.getHeader("User-Agent"); + assertThat(header).matches("^sinch-sdk/.* \\(Java/.*; Apache; .*\\)$"); + } +} diff --git a/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTestIT.java b/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTestIT.java index 20e94b60..bfa90e91 100644 --- a/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTestIT.java +++ b/client/src/test/java/com/sinch/sdk/core/adapters/apache/HttpClientTestIT.java @@ -177,4 +177,50 @@ void bearerAutoRefresh() throws InterruptedException { header = recordedRequest.getHeader("Authorization"); assertEquals(header, "Bearer another token"); } + + @Test + void httpRequestHeaders() throws InterruptedException { + String key = "My-OAS-Header-Key"; + String value = "OAS Header Value"; + Map httpRequest = + Stream.of(new String[][] {{key, value}}) + .collect(Collectors.toMap(data -> data[0], data -> data[1])); + + mockBackEnd.enqueue( + new MockResponse().setBody("foo").addHeader("Content-Type", "application/json")); + + try { + httpClient.invokeAPI( + new ServerConfiguration(String.format("%s/foo/", serverUrl)), + new HttpRequest("foo-path", HttpMethod.GET, null, null, httpRequest, null, null, null)); + } catch (ApiException ae) { + // noop + } + RecordedRequest recordedRequest = mockBackEnd.takeRequest(); + String header = recordedRequest.getHeader(key); + assertEquals(header, value); + } + + @Test + void sdkHeaders() throws InterruptedException { + String key = "My-Sdk-Header-Key"; + String value = "SDK Header Value"; + httpClient.setRequestHeaders( + Stream.of(new String[][] {{key, value}}) + .collect(Collectors.toMap(data -> data[0], data -> data[1]))); + + mockBackEnd.enqueue( + new MockResponse().setBody("foo").addHeader("Content-Type", "application/json")); + + try { + httpClient.invokeAPI( + new ServerConfiguration(String.format("%s/foo/", serverUrl)), + new HttpRequest("foo-path", HttpMethod.GET, null, null, null, null, null, null)); + } catch (ApiException ae) { + // noop + } + RecordedRequest recordedRequest = mockBackEnd.takeRequest(); + String header = recordedRequest.getHeader(key); + assertEquals(header, value); + } } diff --git a/core/src/main/com/sinch/sdk/core/http/HttpClient.java b/core/src/main/com/sinch/sdk/core/http/HttpClient.java index def19014..83e2f2fe 100644 --- a/core/src/main/com/sinch/sdk/core/http/HttpClient.java +++ b/core/src/main/com/sinch/sdk/core/http/HttpClient.java @@ -2,6 +2,7 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.core.models.ServerConfiguration; +import java.util.Map; public interface HttpClient extends AutoCloseable { @@ -9,6 +10,13 @@ public interface HttpClient extends AutoCloseable { void close() throws Exception; + /** + * Register a set of headers to be added onto requests + * + * @param headers Map of key/value headers to be added + */ + void setRequestHeaders(Map headers); + HttpResponse invokeAPI(ServerConfiguration serverConfiguration, HttpRequest request) throws ApiException; } diff --git a/pom.xml b/pom.xml index 978f7d8c..11bf5fd1 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,12 @@ + + + client/resources + true + + @@ -374,6 +380,12 @@ test + + org.apache.maven + maven-model + 3.9.5 + +