Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

- adds headers inspection middleware #575

Merged
merged 3 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

## [0.7.0] - 2023-08-18

### Added

- Added headers inspection option and handler.

## [0.6.0] - 2023-08-08

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

class CaseInsensitiveMap implements Map<String, Set<String>>{
/**
* A map that is case insensitive on the keys
*/
public class CaseInsensitiveMap implements Map<String, Set<String>>{
private final HashMap<String, HashSet<String>> internalMap = new HashMap<>();

/**
* Formats the string to lower case
* @param key string to normalize to lower case
* @return The normalized string
*/
@Nonnull
protected String normalizeKey(@Nonnull final String key) {
Objects.requireNonNull(key);
return key.toLowerCase(Locale.ROOT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@

import jakarta.annotation.Nonnull;

class Headers extends CaseInsensitiveMap {
/**
* A class representing the headers of a request or a response.
*/
public abstract class Headers extends CaseInsensitiveMap {
/** Default constructor */
public Headers() {
protected Headers() {
super();
}

/** Copy constructor */
public Headers(@Nonnull Headers headers) {
/** Copy constructor
* @param headers The headers to initialize with.
*/
protected Headers(@Nonnull Headers headers) {
super();
Objects.requireNonNull(headers);
putAll(headers);
Expand Down
5 changes: 5 additions & 0 deletions components/http/okHttp/spotBugsExcludeFilter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu
<Bug pattern="EI_EXPOSE_REP2" />
<Or>
<Class name="com.microsoft.kiota.http.middleware.UserAgentHandler" />
<Class name="com.microsoft.kiota.http.middleware.HeadersInspectionHandler" />
<Class name="com.microsoft.kiota.http.OkHttpRequestAdapter" />
</Or>
</Match>
<Match>
<Bug pattern="EI_EXPOSE_REP" />
<Class name="com.microsoft.kiota.http.middleware.options.HeadersInspectionOption" />
</Match>
<Match>
<Bug pattern="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" />
<Class name="com.microsoft.kiota.http.OkHttpRequestAdapter" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.microsoft.kiota.http.middleware.RedirectHandler;
import com.microsoft.kiota.http.middleware.RetryHandler;
import com.microsoft.kiota.http.middleware.UserAgentHandler;
import com.microsoft.kiota.http.middleware.HeadersInspectionHandler;
import com.microsoft.kiota.http.middleware.ParametersNameDecodingHandler;

import okhttp3.Interceptor;
Expand Down Expand Up @@ -50,7 +51,8 @@ public static Interceptor[] createDefaultInterceptors() {
new RedirectHandler(),
new RetryHandler(),
new ParametersNameDecodingHandler(),
new UserAgentHandler()
new UserAgentHandler(),
new HeadersInspectionHandler()
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.microsoft.kiota.http.middleware;

import jakarta.annotation.Nonnull;
import kotlin.Pair;

import java.io.IOException;
import java.util.Objects;
import java.util.Set;

import com.microsoft.kiota.http.middleware.options.HeadersInspectionOption;
import com.microsoft.kiota.http.middleware.options.RetryHandlerOption;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* The middleware responsible for inspecting the request and response headers
*/
public class HeadersInspectionHandler implements Interceptor {
/**
* Create a new instance of the HeadersInspectionHandler class with the default options
*/
public HeadersInspectionHandler() {
this(new HeadersInspectionOption());
}
/**
* Create a new instance of the HeadersInspectionHandler class with the provided options
* @param options The options to use for the handler
*/
public HeadersInspectionHandler(@Nonnull final HeadersInspectionOption options) {
this.options = Objects.requireNonNull(options);
}
private final HeadersInspectionOption options;

/** {@inheritDoc} */
@Nonnull
@Override
@SuppressWarnings("UnknownNullness")
public Response intercept(final Chain chain) throws IOException {
Objects.requireNonNull(chain, "parameter chain cannot be null");
Request request = chain.request();
HeadersInspectionOption inspectionOption = request.tag(HeadersInspectionOption.class);
if(inspectionOption == null) { inspectionOption = options; }
final Span span = ObservabilityHelper.getSpanForRequest(request, "HeadersInspectionHandler_Intercept");
Scope scope = null;
if (span != null) {
scope = span.makeCurrent();
span.setAttribute("com.microsoft.kiota.handler.headersInspection.enable", true);
}
try {
if (span != null) {
request = request.newBuilder().tag(Span.class, span).build();
}
if (inspectionOption.getInspectRequestHeaders()) {
for(final Pair<? extends String, ? extends String> header : request.headers()) {
inspectionOption.getRequestHeaders().put(header.getFirst(), Set.of(header.getSecond()));
}
}
final Response response = chain.proceed(request);
if (inspectionOption.getInspectResponseHeaders()) {
for(final Pair<? extends String, ? extends String> header : response.headers()) {
inspectionOption.getResponseHeaders().put(header.getFirst(), Set.of(header.getSecond()));
}
}
return response;
} finally {
if (scope != null) {
scope.close();
}
if (span != null) {
span.end();
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.microsoft.kiota.http.middleware.options;

import jakarta.annotation.Nonnull;

import com.microsoft.kiota.RequestHeaders;
import com.microsoft.kiota.RequestOption;
import com.microsoft.kiota.ResponseHeaders;

/**
* The options to be passed to the headers inspection middleware.
*/
public class HeadersInspectionOption implements RequestOption {
private boolean inspectRequestHeaders;
/**
* Gets whether to inspect request headers
* @return Whether to inspect request headers
*/
public boolean getInspectRequestHeaders() {
return inspectRequestHeaders;
}
/**
* Sets whether to inspect request headers
* @param inspectRequestHeaders Whether to inspect request headers
*/
public void setInspectRequestHeaders(boolean inspectRequestHeaders) {
this.inspectRequestHeaders = inspectRequestHeaders;
}

private boolean inspectResponseHeaders;
/**
* Gets whether to inspect response headers
* @return Whether to inspect response headers
*/
public boolean getInspectResponseHeaders() {
return inspectResponseHeaders;
}
/**
* Sets whether to inspect response headers
* @param inspectResponseHeaders Whether to inspect response headers
*/
public void setInspectResponseHeaders(boolean inspectResponseHeaders) {
this.inspectResponseHeaders = inspectResponseHeaders;
}

private final RequestHeaders requestHeaders = new RequestHeaders();
private final ResponseHeaders responseHeaders = new ResponseHeaders();
/**
* Create default instance of headers inspection options, with default values of inspectRequestHeaders and inspectResponseHeaders.
*/
public HeadersInspectionOption() {
this(false, false);
}
/**
* Create an instance with provided values
* @param shouldInspectResponseHeaders Whether to inspect response headers
* @param shouldInspectRequestHeaders Whether to inspect request headers
*/
public HeadersInspectionOption(final boolean shouldInspectRequestHeaders, final boolean shouldInspectResponseHeaders) {
this.inspectResponseHeaders = shouldInspectResponseHeaders;
this.inspectRequestHeaders = shouldInspectRequestHeaders;
}
/**
* Get the request headers
* @return The request headers
*/
@Nonnull
public RequestHeaders getRequestHeaders() {
return this.requestHeaders;
}
/**
* Get the response headers
* @return The response headers
*/
@Nonnull
public ResponseHeaders getResponseHeaders() {
return this.responseHeaders;
}

/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
@Nonnull
public <T extends RequestOption> Class<T> getType() {
return (Class<T>) HeadersInspectionOption.class;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public UserAgentHandlerOption() { }
@Nonnull
private String productName = "kiota-java";
@Nonnull
private String productVersion = "0.6.0";
private String productVersion = "0.7.0";
/**
* Gets the product name to be used in the user agent header
* @return the product name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.microsoft.kiota.http;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;

import org.junit.jupiter.api.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.microsoft.kiota.http.middleware.HeadersInspectionHandler;
import com.microsoft.kiota.http.middleware.options.HeadersInspectionOption;

import okhttp3.Headers;
import okhttp3.Interceptor.Chain;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

class HeadersInspectionHandlerTest {
private final Chain mockChain;
private final Response mockResponse;
public HeadersInspectionHandlerTest() throws IOException {
mockResponse = mock(Response.class);
when(mockResponse.code()).thenReturn(200);
when(mockResponse.message()).thenReturn("OK");
when(mockResponse.body()).thenReturn(mock(ResponseBody.class));
when(mockResponse.headers()).thenReturn(new Headers.Builder().add("test", "test").build());
mockChain = mock(Chain.class);
when(mockChain.proceed(any(Request.class))).thenAnswer(new Answer<Response>() {
public Response answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Request request = (Request) args[0];
when(mockResponse.request()).thenReturn(request);
return mockResponse;
}
});
}
@Test
void instantiatesWithDefaults() {
final HeadersInspectionHandler handler = new HeadersInspectionHandler();
assertNotNull(handler);
}
@Test
void getsRequestHeaders() throws IOException {
final HeadersInspectionOption option = new HeadersInspectionOption(true, false);
final HeadersInspectionHandler handler = new HeadersInspectionHandler(option);
final Request request = new Request.Builder().url("http://localhost").addHeader("test", "test").build();
when(mockChain.request()).thenReturn(request);
handler.intercept(mockChain);
assertNotNull(option.getRequestHeaders());
assertNotNull(option.getResponseHeaders());
assertEquals(1, option.getRequestHeaders().size());
assertEquals(0, option.getResponseHeaders().size());
assertEquals("test", option.getRequestHeaders().get("test").toArray()[0]);
}
@Test
void getsResponseHeaders() throws IOException {
final HeadersInspectionOption option = new HeadersInspectionOption(false, true);
final HeadersInspectionHandler handler = new HeadersInspectionHandler(option);
final Request request = new Request.Builder().url("http://localhost").addHeader("test", "test").build();
when(mockChain.request()).thenReturn(request);
handler.intercept(mockChain);
assertNotNull(option.getRequestHeaders());
assertNotNull(option.getResponseHeaders());
assertEquals(0, option.getRequestHeaders().size());
assertEquals(1, option.getResponseHeaders().size());
assertEquals("test", option.getResponseHeaders().get("test").toArray()[0]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import com.microsoft.kiota.http.middleware.UserAgentHandler;
import com.microsoft.kiota.http.middleware.options.UserAgentHandlerOption;

public class UserAgentHandlerTest {
class UserAgentHandlerTest {
private final Chain mockChain;
private final Response mockResponse;
public UserAgentHandlerTest() throws IOException {
Expand All @@ -39,10 +39,10 @@ public Object answer(InvocationOnMock invocation) {
when(mockResponse.request()).thenReturn(request);
return mockResponse;
}
});
});
}
@Test
public void addsTheProduct() throws IOException {
void addsTheProduct() throws IOException {
final UserAgentHandler handler = new UserAgentHandler();
final Request request = new Request.Builder().url("http://localhost").build();
when(mockChain.request()).thenReturn(request);
Expand All @@ -53,7 +53,7 @@ public void addsTheProduct() throws IOException {
assertEquals("kiota-java", result.header("User-Agent").split("/")[0]);
}
@Test
public void addsTheProductOnce() throws IOException {
void addsTheProductOnce() throws IOException {
final UserAgentHandler handler = new UserAgentHandler();
final Request request = new Request.Builder().url("http://localhost").build();
when(mockChain.request()).thenReturn(request);
Expand All @@ -65,7 +65,7 @@ public void addsTheProductOnce() throws IOException {
assertEquals(1, result.header("User-Agent").split("kiota-java").length - 1);
}
@Test
public void doesNotAddTheProductWhenDisabled() throws IOException {
void doesNotAddTheProductWhenDisabled() throws IOException {
final UserAgentHandler handler = new UserAgentHandler(new UserAgentHandlerOption() {{ setEnabled(false); }});
final Request request = new Request.Builder().url("http://localhost").build();
when(mockChain.request()).thenReturn(request);
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ org.gradle.caching=true

mavenGroupId = com.microsoft.kiota
mavenMajorVersion = 0
mavenMinorVersion = 6
mavenMinorVersion = 7
mavenPatchVersion = 0
mavenArtifactSuffix =

Expand Down
Loading