diff --git a/pom.xml b/pom.xml index f316d230..27b47f6e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ @@ -109,14 +109,26 @@ maven-war-plugin ${maven-war-plugin-version} + + + notices + package + + war + + + false + ${configuration.directory} + + + ${project.basedir}/../src/main/notices + + + + + false - ${configuration.directory} - - - ${project.basedir}/../src/main/notices - - diff --git a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/AuthenticatedCall.java b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/AuthenticatedCall.java index cc68b65a..d49cfe7b 100644 --- a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/AuthenticatedCall.java +++ b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/AuthenticatedCall.java @@ -1,15 +1,24 @@ -// Copyright (c) 2017, 2022, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; import java.io.IOException; import java.io.PrintStream; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; import com.oracle.wls.exporter.domain.MBeanSelector; import com.oracle.wls.exporter.domain.QueryType; +import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_CHALLENGE_HEADER; +import static com.oracle.wls.exporter.WebAppConstants.COOKIE_HEADER; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; @@ -19,10 +28,26 @@ * and the WLS RESTful Management services, thus using that service's security. */ public abstract class AuthenticatedCall { + + /** + * The length of time in seconds after which a cookie is deemed to have expired. + */ + static final long COOKIE_LIFETIME_SECONDS = 1000L; + + /** + * A map of authentication credentials to lists of cookies. The cookies will be sent on any request with + * the specified credentials. + */ + private static final Map> COOKIES = new HashMap<>(); + private final WebClientFactory webClientFactory; private final InvocationContext context; private final UrlBuilder urlBuilder; + // For unit testing only + static void clearCookies() { + COOKIES.clear(); + } protected AuthenticatedCall(WebClientFactory webClientFactory, InvocationContext context) { this.webClientFactory = webClientFactory; @@ -46,13 +71,63 @@ public WebClient createWebClient() { final WebClient webClient = webClientFactory.createClient(); webClient.addHeader("X-Requested-By", "rest-exporter"); - setAuthentication(webClient); + webClient.setAuthentication(context.getAuthenticationHeader()); + manageCookies(webClient); return webClient; } - protected void setAuthentication(WebClient webClient) { - webClient.setAuthentication(context.getAuthenticationHeader()); - } + private void manageCookies(WebClient webClient) { + synchronized (COOKIES) { + getCookies(context.getAuthenticationHeader()).forEach(c -> webClient.addHeader(COOKIE_HEADER, c)); + webClient.onSetCookieReceivedDo(this::handleNewCookie); + webClient.onSetCookieReceivedDo(c -> webClient.addHeader(COOKIE_HEADER, c)); + } + } + + public List getCookies(String credentials) { + final OffsetDateTime now = SystemClock.now(); + final List cookieList = getCookieList(credentials); + cookieList.removeIf(c -> c.isExpiredAt(now)); + + return cookieList.stream().map(Cookie::getValue).collect(Collectors.toList()); + } + + private List getCookieList(String credentials) { + return Optional.ofNullable(COOKIES.get(credentials)).orElse(Collections.emptyList()); + } + + void handleNewCookie(String cookieHeader) { + if (context.getAuthenticationHeader() == null) return; + + final Cookie cookie = new Cookie(cookieHeader); + COOKIES + .computeIfAbsent(context.getAuthenticationHeader(), h -> new ArrayList<>()) + .add(cookie); + } + + private static class Cookie { + private final String value; + private final OffsetDateTime expirationTime = SystemClock.now().plusSeconds(COOKIE_LIFETIME_SECONDS); + + Cookie(String cookieHeader) { + this.value = trimParameters(cookieHeader); + } + + String getValue() { + return value; + } + + boolean isExpiredAt(OffsetDateTime now) { + return now.isAfter(expirationTime); + } + + private String trimParameters(String cookieHeader) { + if (!cookieHeader.contains(";")) + return cookieHeader; + else + return cookieHeader.substring(0, cookieHeader.indexOf(';')); + } + } /** * Performs a servlet action, wrapping it with authentication handling. @@ -71,7 +146,7 @@ public void doWithAuthentication() throws IOException { } catch (ForbiddenException e) { context.sendError(HTTP_FORBIDDEN, "Not authorized"); } catch (AuthenticationChallengeException e) { - context.setResponseHeader("WWW-Authenticate", e.getChallenge()); + context.setResponseHeader(AUTHENTICATION_CHALLENGE_HEADER, e.getChallenge()); context.sendError(HTTP_UNAUTHORIZED, "Authentication required"); } catch (ServerErrorException e) { final int status = e.getStatus(); diff --git a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/SystemClock.java b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/SystemClock.java new file mode 100644 index 00000000..12121c22 --- /dev/null +++ b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/SystemClock.java @@ -0,0 +1,37 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.wls.exporter; + +import java.time.OffsetDateTime; + +/** + * A wrapper for the system clock that facilitates unit testing of time. + */ +public abstract class SystemClock { + + // Leave as non-final; unit tests may replace this value + @SuppressWarnings({"FieldMayBeFinal", "CanBeFinal"}) + private static SystemClock delegate = new SystemClock() { + @Override + public OffsetDateTime getCurrentTime() { + return OffsetDateTime.now(); + } + }; + + /** + * Returns the current time. + * + * @return a time instance + */ + public static OffsetDateTime now() { + return delegate.getCurrentTime(); + } + + /** + * Returns the delegate's current time. + * + * @return a time instance + */ + public abstract OffsetDateTime getCurrentTime(); +} diff --git a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebAppConstants.java b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebAppConstants.java index ad343c85..a9e528c1 100644 --- a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebAppConstants.java +++ b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebAppConstants.java @@ -1,4 +1,4 @@ -// Copyright (c) 2017, 2021, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; @@ -25,9 +25,18 @@ public interface WebAppConstants { String MESSAGES_PAGE = "messages"; String LOG_PAGE = "log"; + /** The header sent by a web server to require authentication. **/ + String AUTHENTICATION_CHALLENGE_HEADER = "WWW-Authenticate"; + + /** The header used by a web server to define a new cookie. **/ + String SET_COOKIE_HEADER = "Set-Cookie"; + /** The header used by a web client to send its authentication credentials. **/ String AUTHENTICATION_HEADER = "Authorization"; + /** The header used by a web client to pass a created cookie to a server. **/ + String COOKIE_HEADER = "Cookie"; + /** The header used by a web client to specify the content type of its data. **/ String CONTENT_TYPE_HEADER = "Content-Type"; @@ -35,7 +44,7 @@ public interface WebAppConstants { String EFFECT_OPTION = "effect"; // The possible values for the effect - String DEFAULT_ACTION = WebAppConstants.REPLACE_ACTION; String REPLACE_ACTION = "replace"; String APPEND_ACTION = "append"; + String DEFAULT_ACTION = REPLACE_ACTION; } diff --git a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClient.java b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClient.java index 92e6fd21..5fba6b2a 100644 --- a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClient.java +++ b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClient.java @@ -1,9 +1,10 @@ -// Copyright (c) 2017, 2021, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; import java.io.IOException; +import java.util.function.Consumer; /** * An interface for an object responsible for sending requests from exporter web application to @@ -72,4 +73,16 @@ public interface WebClient { * Returns true if this client has been marked to retry the last request. */ boolean isRetryNeeded(); + + /** + * Defines a handler to be called when a Set-Cookie header is found in a reply from the server. + * @param setCookieHandler the handler to call with the value of the header + */ + void onSetCookieReceivedDo(Consumer setCookieHandler); + + + interface Response { + String getBody() throws IOException; + + } } diff --git a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClientCommon.java b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClientCommon.java index a0a4cec9..95245a44 100644 --- a/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClientCommon.java +++ b/wls-exporter-core/src/main/java/com/oracle/wls/exporter/WebClientCommon.java @@ -1,4 +1,4 @@ -// Copyright (c) 2017, 2022, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; @@ -11,11 +11,17 @@ import java.net.URI; import java.net.UnknownHostException; import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_CHALLENGE_HEADER; import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_HEADER; import static com.oracle.wls.exporter.WebAppConstants.CONTENT_TYPE_HEADER; +import static com.oracle.wls.exporter.WebAppConstants.SET_COOKIE_HEADER; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; @@ -32,6 +38,7 @@ public abstract class WebClientCommon implements WebClient { private boolean retryNeeded; private String contentType; private String url; + private final List> setCookieHandlers = new ArrayList<>(); interface WebRequest { String getMethod(); @@ -105,26 +112,26 @@ protected String getContentType() { @Override public String doGetRequest() throws IOException { defineSessionHeaders(); - return sendRequest(createGetRequest(url)); + return sendRequest(createGetRequest(url)).getBody(); } @Override public String doPostRequest(String postBody) throws IOException { if (contentType == null) contentType = APPLICATION_JSON; defineSessionHeaders(); - return sendRequest(createPostRequest(url, postBody)); + return sendRequest(createPostRequest(url, postBody)).getBody(); } @Override public String doPutRequest(T putBody) throws IOException { defineSessionHeaders(); - return sendRequest(createPutRequest(url, putBody)); + return sendRequest(createPutRequest(url, putBody)).getBody(); } // Sends the specified request to the server - private String sendRequest(WebRequest request) throws IOException { + private ResponseImpl sendRequest(WebRequest request) throws IOException { try (HttpClientExec clientExec = createClientExec()) { - return getReply(clientExec.send(request)); + return new ResponseImpl(clientExec.send(request)); } catch (UnknownHostException | ConnectException e) { throw new RestPortConnectionException(request.getURI().toString()); } catch (GeneralSecurityException e) { @@ -132,61 +139,12 @@ private String sendRequest(WebRequest request) throws IOException { } } - private String getReply(WebResponse response) throws IOException { - processStatusCode(response); - try (final InputStream contents = response.getContents()) { - return toString(contents); - } - } - - private void processStatusCode(WebResponse response) { - switch (response.getResponseCode()) { - case HTTP_BAD_REQUEST: - throw new RestQueryException(); - case HTTP_UNAUTHORIZED: - throw createAuthenticationChallengeException(response); - case HTTP_FORBIDDEN: - throw new ForbiddenException(); - default: - if (response.getResponseCode() > SC_BAD_REQUEST) - throw createServerErrorException(response); - } - } - - private ServerErrorException createServerErrorException(WebResponse response) { - try { - return new ServerErrorException(response.getResponseCode(), toString(response.getContents())); - } catch (IOException e) { - return new ServerErrorException(response.getResponseCode()); - } - } - - // Converts an input stream to a string. - private String toString(InputStream inputStream) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[4096]; - int numBytes = 0; - while (numBytes >= 0) { - baos.write(buffer, 0, numBytes); - numBytes = inputStream.read(buffer); - } - return baos.toString("UTF8"); - } - final void defineSessionHeaders() { clearSessionHeaders(); if (getAuthentication() != null) putSessionHeader(AUTHENTICATION_HEADER, getAuthentication()); if (getContentType() != null) putSessionHeader(CONTENT_TYPE_HEADER, getContentType()); } - private AuthenticationChallengeException createAuthenticationChallengeException(WebResponse response) { - return new AuthenticationChallengeException(getAuthenticationHeader(response)); - } - - private String getAuthenticationHeader(WebResponse response) { - return response.getHeadersAsStream("WWW-Authenticate").filter(Objects::nonNull).findFirst().orElse(null); - } - static class EmptyInputStream extends InputStream { @Override public int read() { @@ -236,4 +194,83 @@ public boolean isRetryNeeded() { retryNeeded = false; } } + + @Override + public void onSetCookieReceivedDo(Consumer setCookieHandler) { + setCookieHandlers.add(setCookieHandler); + } + + protected void invokeSetCookieHandlerCallbacks(List setCookieHeaders) { + setCookieHandlers.forEach(setCookieHeaders::forEach); + } + + class ResponseImpl implements WebClient.Response { + private final WebResponse response; + private final String body; + + ResponseImpl(WebResponse response) throws IOException { + this.response = response; + processStatusCode(); + reportSetCookieHeaders(); + + try (final InputStream contents = response.getContents()) { + body = asString(contents); + } + } + + private void processStatusCode() { + switch (response.getResponseCode()) { + case HTTP_BAD_REQUEST: + throw new RestQueryException(); + case HTTP_UNAUTHORIZED: + throw createAuthenticationChallengeException(); + case HTTP_FORBIDDEN: + throw new ForbiddenException(); + default: + if (response.getResponseCode() > SC_BAD_REQUEST) + throw createServerErrorException(); + } + } + + private void reportSetCookieHeaders() { + invokeSetCookieHandlerCallbacks(getSetCookieHeaders()); + } + + private List getSetCookieHeaders() { + return response.getHeadersAsStream(SET_COOKIE_HEADER).filter(Objects::nonNull).collect(Collectors.toList()); + } + + private ServerErrorException createServerErrorException() { + try { + return new ServerErrorException(response.getResponseCode(), asString(response.getContents())); + } catch (IOException e) { + return new ServerErrorException(response.getResponseCode()); + } + } + + private AuthenticationChallengeException createAuthenticationChallengeException() { + return new AuthenticationChallengeException(getAuthenticationHeader()); + } + + private String getAuthenticationHeader() { + return response.getHeadersAsStream(AUTHENTICATION_CHALLENGE_HEADER).filter(Objects::nonNull).findFirst().orElse(null); + } + + private String asString(InputStream inputStream) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int numBytes = 0; + while (numBytes >= 0) { + baos.write(buffer, 0, numBytes); + numBytes = inputStream.read(buffer); + } + return baos.toString("UTF8"); + } + + @Override + public String getBody() { + return body; + } + + } } diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/AuthenticatedCallTest.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/AuthenticatedCallTest.java new file mode 100644 index 00000000..99bb558a --- /dev/null +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/AuthenticatedCallTest.java @@ -0,0 +1,135 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.wls.exporter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.meterware.simplestub.Memento; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.oracle.wls.exporter.InvocationContextStub.CREDENTIALS; +import static com.oracle.wls.exporter.WebAppConstants.COOKIE_HEADER; +import static com.oracle.wls.exporter.WebAppConstants.SET_COOKIE_HEADER; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; + +class AuthenticatedCallTest { + + private final WebClientFactoryStub webClientFactory = new WebClientFactoryStub(); + private final InvocationContextStub invocationContext = InvocationContextStub.create(); + private final AuthenticatedCallStub callStub = new AuthenticatedCallStub(webClientFactory, invocationContext); + private final List mementos = new ArrayList<>(); + + @BeforeEach + void setUp() throws NoSuchFieldException { + AuthenticatedCall.clearCookies(); + + mementos.add(SystemClockTestSupport.installClock()); + } + + @AfterEach + void tearDown() { + mementos.forEach(Memento::revert); + } + + @Test + void whenInvocationContextContainsNoCookieHeaders_callToServerContinues() throws IOException { + + callStub.doWithAuthentication(); + + assertThat(webClientFactory.getSentHeaders(COOKIE_HEADER), empty()); + } + + @Test + void withNoAuthenticationWhenResponseFromRestApiContainsSetCookieHeaders_ignoreThem() throws IOException { + invocationContext.setAuthenticationHeader(null); + configureResponseWithSetCookieHeaders(); + + callStub.doWithAuthentication(); + + assertThat(callStub.getCookies(CREDENTIALS), empty()); + } + + private void configureResponseWithSetCookieHeaders() { + webClientFactory.forJson("{}") + .withResponseHeader(SET_COOKIE_HEADER, "cookie1=value1; http-only") + .withResponseHeader(SET_COOKIE_HEADER, "cookie2=value2; secure") + .addResponse(); + } + + @Test + void whenResponseFromRestApiContainsSetCookieHeaders_cacheThem() throws IOException { + configureResponseWithSetCookieHeaders(); + + callStub.doWithAuthentication(); + + assertThat(callStub.getCookies(CREDENTIALS), containsInAnyOrder("cookie1=value1", "cookie2=value2")); + } + + @Test + void whenResponseFromRestApiContainsSetCookieHeaders_ignoreForDifferentCredentials() throws IOException { + configureResponseWithSetCookieHeaders(); + + callStub.doWithAuthentication(); + + assertThat(callStub.getCookies("other credentials"), empty()); + } + + @Test + void whenInvocationContextContainsCookieHeaders_forwardThemToTheServer() throws IOException { + callStub.handleNewCookie("cookie1=value1; http-only"); + callStub.handleNewCookie("cookie2=value2; secure"); + webClientFactory.forJson("{}").addResponse(); + + callStub.doWithAuthentication(); + + final List cookieHeaders + = webClientFactory.getSentHeaders(COOKIE_HEADER).stream() + .map(this::trimParameters) + .collect(Collectors.toList()); + assertThat(cookieHeaders, containsInAnyOrder("cookie1=value1", "cookie2=value2")); + } + + @Test + void wheCookiesExpired_removeThem() throws IOException { + callStub.handleNewCookie("cookie1=value1; http-only"); + callStub.handleNewCookie("cookie2=value2; secure"); + webClientFactory.forJson("{}").addResponse(); + + SystemClockTestSupport.increment(AuthenticatedCall.COOKIE_LIFETIME_SECONDS + 1); + callStub.doWithAuthentication(); + + final List cookieHeaders + = webClientFactory.getSentHeaders(COOKIE_HEADER).stream() + .map(this::trimParameters) + .collect(Collectors.toList()); + assertThat(cookieHeaders, empty()); + } + + private String trimParameters(String cookieHeader) { + if (!cookieHeader.contains(";")) + return cookieHeader; + else + return cookieHeader.substring(0, cookieHeader.indexOf(';')); + } + + static class AuthenticatedCallStub extends AuthenticatedCall { + AuthenticatedCallStub(WebClientFactory webClientFactory, InvocationContext context) { + super(webClientFactory, context); + } + + @Override + protected void invoke(WebClient webClient, InvocationContext context) throws IOException { + webClient.withUrl(getAuthenticationUrl()).doPostRequest("abcd"); + } + } + + +} \ No newline at end of file diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ConfigurationFormCallTest.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ConfigurationFormCallTest.java index 37e2482c..7823e053 100644 --- a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ConfigurationFormCallTest.java +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ConfigurationFormCallTest.java @@ -1,4 +1,4 @@ -// Copyright (c) 2021, 2022, Oracle and/or its affiliates. +// Copyright (c) 2021, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; @@ -12,6 +12,7 @@ import static com.oracle.wls.exporter.InvocationContextStub.HOST_NAME; import static com.oracle.wls.exporter.InvocationContextStub.PORT; import static com.oracle.wls.exporter.InvocationContextStub.REST_PORT; +import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_CHALLENGE_HEADER; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static org.hamcrest.MatcherAssert.assertThat; @@ -184,6 +185,6 @@ void whenServerSends401StatusOnGet_returnToClient() throws Exception { handleConfigurationFormCall(context.withConfigurationForm("replace", CONFIGURATION)); assertThat(context.getResponseStatus(), equalTo(HTTP_UNAUTHORIZED)); - assertThat(context.getResponseHeader("WWW-Authenticate"), equalTo("Basic realm=\"Test-Realm\"")); + assertThat(context.getResponseHeader(AUTHENTICATION_CHALLENGE_HEADER), equalTo("Basic realm=\"Test-Realm\"")); } } \ No newline at end of file diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterCallTest.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterCallTest.java index 077498d5..7be68ae7 100644 --- a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterCallTest.java +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterCallTest.java @@ -1,16 +1,19 @@ -// Copyright (c) 2021, 2022, Oracle and/or its affiliates. +// Copyright (c) 2021, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; import java.io.IOException; +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static com.oracle.wls.exporter.InvocationContextStub.HOST_NAME; import static com.oracle.wls.exporter.InvocationContextStub.PORT; +import static com.oracle.wls.exporter.WebAppConstants.COOKIE_HEADER; +import static com.oracle.wls.exporter.WebAppConstants.SET_COOKIE_HEADER; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; @@ -22,6 +25,8 @@ class ExporterCallTest { private static final String ONE_VALUE_CONFIG = "queries:\n- groups:\n key: name\n values: testSample1"; private static final String CONFIG_WITH_FILTER = "queries:" + "\n- groups:\n key: name\n includedKeyValues: abc.*\n values: testSample1"; + private static final String DUAL_QUERY_CONFIG = ONE_VALUE_CONFIG + + "\n- clubs:\n key: name\n values: testSample2"; private static final String KEY_RESPONSE_JSON = "{\"groups\": {\"items\": [\n" + " {\"name\": \"alpha\"},\n" + @@ -29,12 +34,25 @@ class ExporterCallTest { " {\"name\": \"gamma\"}\n" + "]}}"; + private static final String QUERY_RESPONSE1_JSON = "{\"groups\": {\"items\": [\n" + + " {\"name\": \"alpha\", \"testSample1\": \"first\"},\n" + + " {\"name\": \"beta\", \"testSample1\": \"second\"},\n" + + " {\"name\": \"gamma\", \"testSample1\": \"third\"}\n" + + "]}}"; + + private static final String QUERY_RESPONSE2_JSON = "{\"clubs\": {\"items\": [\n" + + " {\"name\": \"aleph\", \"testSample2\": \"first\"},\n" + + " {\"name\": \"bet\", \"testSample2\": \"second\"},\n" + + " {\"name\": \"gimel\", \"testSample2\": \"third\"}\n" + + "]}}"; + private final WebClientFactoryStub factory = new WebClientFactoryStub(); private final InvocationContextStub context = InvocationContextStub.create(); @BeforeEach public void setUp() { LiveConfiguration.setServer(HOST_NAME, PORT); + AuthenticatedCall.clearCookies(); } @Test @@ -98,4 +116,15 @@ void whenQuerySentWithFilter_twoRequestsAreMade() throws IOException { assertThat(factory.getNumQueriesSent(), equalTo(2)); } + + @Test + void whenHaveMultipleQueries_sendServerDefinedCookieOnSecondQuery() throws IOException { + factory.forJson(QUERY_RESPONSE1_JSON).withResponseHeader(SET_COOKIE_HEADER, "cookieName=newValue").addResponse(); + factory.addJsonResponse(QUERY_RESPONSE2_JSON); + LiveConfiguration.loadFromString(DUAL_QUERY_CONFIG); + + handleMetricsCall(context); + + assertThat(factory.getSentHeaders(COOKIE_HEADER), Matchers.hasItem("cookieName=newValue")); + } } \ No newline at end of file diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterServletTest.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterServletTest.java index a27e48c1..adff930c 100644 --- a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterServletTest.java +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/ExporterServletTest.java @@ -1,14 +1,14 @@ -// Copyright (c) 2017, 2022, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; import java.io.IOException; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; import com.google.common.collect.ImmutableMap; import com.oracle.wls.exporter.webapp.ExporterServlet; @@ -21,6 +21,7 @@ import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static com.oracle.wls.exporter.InMemoryFileSystem.withNoParams; +import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_CHALLENGE_HEADER; import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_HEADER; import static com.oracle.wls.exporter.matchers.CommentsOnlyMatcher.containsOnlyComments; import static com.oracle.wls.exporter.matchers.MetricsNamesSnakeCaseMatcher.usesSnakeCase; @@ -37,7 +38,6 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -213,7 +213,7 @@ void whenServerSends401StatusOnGet_returnToClient() throws Exception { servlet.doGet(request, response); assertThat(response.getStatus(), equalTo(SC_UNAUTHORIZED)); - assertThat(response, containsHeader("WWW-Authenticate", "Basic realm=\"Test-Realm\"")); + assertThat(response, containsHeader(AUTHENTICATION_CHALLENGE_HEADER, "Basic realm=\"Test-Realm\"")); } @Test @@ -256,7 +256,7 @@ void onGet_sendXRequestedHeader() throws Exception { servlet.doGet(request, response); - assertThat(factory.getSentHeaders(), hasKey("X-Requested-By")); + assertThat(factory.getSentHeaders("X-Requested-By"), not(empty())); } @Test diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/InvocationContextStub.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/InvocationContextStub.java index 65c9fa45..812263b6 100644 --- a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/InvocationContextStub.java +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/InvocationContextStub.java @@ -1,4 +1,4 @@ -// Copyright (c) 2021, 2022, Oracle and/or its affiliates. +// Copyright (c) 2021, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; @@ -8,8 +8,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import static com.meterware.simplestub.Stub.createStrictStub; @@ -18,15 +22,18 @@ abstract class InvocationContextStub implements InvocationContext { static final String HOST_NAME = "myhost"; static final int PORT = 7123; static final int REST_PORT = 7431; + static final String CREDENTIALS = "Basic stuff"; + private final ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); - private final String authenticationHeader = null; + private String authenticationHeader = CREDENTIALS; private String contentType = "text/plain"; private String redirectLocation = null; private InputStream requestStream = null; private int responseStatus = 0; private boolean secure; - private final Map responseHeaders = new HashMap<>(); + private final Map> responseHeaders = new HashMap<>(); + private final Map cookies = new HashMap<>(); static InvocationContextStub create() { return createStrictStub(InvocationContextStub.class); @@ -49,6 +56,10 @@ InvocationContextStub withConfiguration(String contentType, String configuration return this; } + void addCookie(String name, String value) { + cookies.put(name, value); + } + String getRedirectLocation() { return redirectLocation; } @@ -59,13 +70,22 @@ String getResponse() { @SuppressWarnings("SameParameterValue") String getResponseHeader(String name) { - return responseHeaders.get(name); + return Optional.ofNullable(responseHeaders.get(name)).map(h-> h.get(0)).orElse(null); + } + + @SuppressWarnings("SameParameterValue") + List getResponseHeaders(String name) { + return Optional.ofNullable(responseHeaders.get(name)).orElse(Collections.emptyList()); } int getResponseStatus() { return responseStatus; } + void setAuthenticationHeader(String authenticationHeader) { + this.authenticationHeader = authenticationHeader; + } + @Override public void close() { } @@ -80,7 +100,6 @@ public String getApplicationContext() { return "/unitTest/"; } - @SuppressWarnings("ConstantConditions") @Override public String getAuthenticationHeader() { return authenticationHeader; @@ -113,7 +132,7 @@ public void sendRedirect(String location) { @Override public void setResponseHeader(String name, String value) { - responseHeaders.put(name, value); + responseHeaders.computeIfAbsent(name, n -> new ArrayList<>()).add(value); } @Override diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/SystemClockTestSupport.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/SystemClockTestSupport.java new file mode 100644 index 00000000..20db406d --- /dev/null +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/SystemClockTestSupport.java @@ -0,0 +1,42 @@ +// Copyright (c) 2023, Oracle and/or its affiliates. +// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. + +package com.oracle.wls.exporter; + +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; + +import com.meterware.simplestub.Memento; +import com.meterware.simplestub.StaticStubSupport; + +public class SystemClockTestSupport { + private static TestSystemClock clock; + + public static Memento installClock() throws NoSuchFieldException { + clock = new TestSystemClock(); + return StaticStubSupport.install(SystemClock.class, "delegate", clock); + } + + /** + * Increments the system clock by the specified number of seconds. + * @param numSeconds the number of seconds by which to advance the system clock + */ + public static void increment(long numSeconds) { + clock.increment(numSeconds); + } + + static class TestSystemClock extends SystemClock { + private final OffsetDateTime testStartTime = SystemClock.now().truncatedTo(ChronoUnit.SECONDS); + private OffsetDateTime currentTime = testStartTime; + + @Override + public OffsetDateTime getCurrentTime() { + return currentTime; + } + + void increment(long numSeconds) { + currentTime = currentTime.plusSeconds(numSeconds); + } + } + +} diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/WebClientFactoryStub.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/WebClientFactoryStub.java index 0058a376..f1252f9f 100644 --- a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/WebClientFactoryStub.java +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/WebClientFactoryStub.java @@ -1,19 +1,20 @@ -// Copyright (c) 2017, 2022, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import com.google.gson.Gson; import static com.meterware.simplestub.Stub.createStrictStub; +import static com.oracle.wls.exporter.WebAppConstants.SET_COOKIE_HEADER; public class WebClientFactoryStub implements WebClientFactory { private final WebClientStub webClient = createStrictStub(WebClientStub.class); @@ -25,6 +26,11 @@ public WebClient createClient() { return webClient; } + interface ResponseBuilder { + ResponseBuilder withResponseHeader(String name, String value); + void addResponse(); + } + void addJsonResponse(Map responseMap) { webClient.addJsonResponse(new Gson().toJson(responseMap)); } @@ -33,6 +39,10 @@ void addJsonResponse(String json) { webClient.addJsonResponse(json); } + ResponseBuilder forJson(String json) { + return webClient.forJson(json); + } + int getNumQueriesSent() { return webClient.jsonQueries.size(); } @@ -49,8 +59,8 @@ String getPostedString() { return webClient.postedString; } - Map getSentHeaders() { - return webClient.sentHeaders; + List getSentHeaders(String headerName) { + return Optional.ofNullable(webClient.sentHeaders.get(headerName)).orElse(Collections.emptyList()); } String getSentAuthentication() { @@ -90,12 +100,12 @@ static abstract class WebClientStub extends WebClientCommon { private final List jsonQueries = new ArrayList<>(); private final List testResponses = new ArrayList<>(); private Iterator responses; - private final Map addedHeaders = new HashMap<>(); - private Map sentHeaders; + private final Map> addedHeaders = new HashMap<>(); + private Map> sentHeaders; private String postedString; - private void addJsonResponse(String jsonResponse) { - addResponse(new JsonResponse(jsonResponse)); + private void addJsonResponse(String responseJson) { + addResponse(new JsonResponse(responseJson)); } private void addResponse(TestResponse response) { @@ -103,6 +113,30 @@ private void addResponse(TestResponse response) { testResponses.add(response); } + private ResponseBuilder forJson(String jsonString) { + return new ResponseBuilderImpl(jsonString); + } + + class ResponseBuilderImpl implements ResponseBuilder { + private final JsonResponse jsonResponse; + + ResponseBuilderImpl(String jsonString) { + this.jsonResponse = new JsonResponse(jsonString); + WebClientStub.this.addedHeaders.clear(); + } + + @Override + public ResponseBuilder withResponseHeader(String name, String value) { + jsonResponse.withResponseHeader(name, value); + return this; + } + + @Override + public void addResponse() { + WebClientStub.this.addResponse(jsonResponse); + } + } + private boolean allResponsesHandled() { return responses != null && !responses.hasNext(); } @@ -130,7 +164,7 @@ void reportAuthenticationRequired(String basicRealmName) { } void throwConnectionFailure(String hostName, int port) { - addExceptionResponse(new RestPortConnectionException(String.format("http://%s:%d", hostName, port))); + addExceptionResponse(new RestPortConnectionException(String.format("https://%s:%d", hostName, port))); } @Override @@ -141,7 +175,7 @@ public WebClientCommon withUrl(String url) { @Override public void addHeader(String name, String value) { - addedHeaders.put(name, value); + addedHeaders.computeIfAbsent(name, n -> new ArrayList<>()).add(value); } @Override @@ -162,7 +196,7 @@ public String doGetRequest() { } @Override - public String doPutRequest(T putBody) throws IOException { + public String doPutRequest(T putBody) { if (url == null) throw new NullPointerException("No URL specified"); postedString = new Gson().toJson(putBody); @@ -176,14 +210,21 @@ private TestResponse getNextResponse() { private String getResult(TestResponse response) { if (response.getException() != null) throw response.getException(); + + invokeSetCookieHandlerCallbacks(getSetCookieHeaders(response)); return response.getJsonResponse(); } + private List getSetCookieHeaders(TestResponse response) { + return response.getResponseHeaders(SET_COOKIE_HEADER); + } + } interface TestResponse { WebClientException getException(); String getJsonResponse(); + List getResponseHeaders(String headerName); } static class ExceptionResponse implements TestResponse { @@ -202,15 +243,26 @@ public WebClientException getException() { public String getJsonResponse() { return null; } + + @Override + public List getResponseHeaders(String headerName) { + return Collections.emptyList(); + } } static class JsonResponse implements TestResponse { String jsonResponse; + private final Map> responseHeaders = new HashMap<>(); JsonResponse(String jsonResponse) { this.jsonResponse = jsonResponse; } + JsonResponse withResponseHeader(String name, String value) { + getResponseHeaders(name).add(value); + return this; + } + @Override public WebClientException getException() { return null; @@ -220,11 +272,11 @@ public WebClientException getException() { public String getJsonResponse() { return jsonResponse; } - } - static class QueryTestException extends WebClientException { - public QueryTestException(String message) { - super(message); + @Override + public List getResponseHeaders(String headerName) { + return responseHeaders.computeIfAbsent(headerName, k -> new ArrayList<>()); } } + } diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/ConfigurationServletTest.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/ConfigurationServletTest.java index 0936c68f..c8f5c336 100644 --- a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/ConfigurationServletTest.java +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/ConfigurationServletTest.java @@ -1,4 +1,4 @@ -// Copyright (c) 2017, 2022, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter.webapp; @@ -22,6 +22,7 @@ import static com.oracle.wls.exporter.MultipartTestUtils.createEncodedForm; import static com.oracle.wls.exporter.MultipartTestUtils.createUploadRequest; +import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_CHALLENGE_HEADER; import static com.oracle.wls.exporter.WebAppConstants.CONFIGURATION_PAGE; import static com.oracle.wls.exporter.matchers.ResponseHeaderMatcher.containsHeader; import static com.oracle.wls.exporter.webapp.HttpServletRequestStub.LOCAL_PORT; @@ -244,6 +245,6 @@ void whenServerSends401StatusOnGet_returnToClient() throws Exception { servlet.doPost(request, response); assertThat(response.getStatus(), equalTo(SC_UNAUTHORIZED)); - assertThat(response, containsHeader("WWW-Authenticate", "Basic realm=\"Test-Realm\"")); + assertThat(response, containsHeader(AUTHENTICATION_CHALLENGE_HEADER, "Basic realm=\"Test-Realm\"")); } } diff --git a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/HttpServletRequestStub.java b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/HttpServletRequestStub.java index 140c8669..f62e625e 100644 --- a/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/HttpServletRequestStub.java +++ b/wls-exporter-core/src/test/java/com/oracle/wls/exporter/webapp/HttpServletRequestStub.java @@ -1,4 +1,4 @@ -// Copyright (c) 2017, 2022, Oracle and/or its affiliates. +// Copyright (c) 2017, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter.webapp; @@ -7,12 +7,15 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Vector; import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @@ -36,8 +39,9 @@ public abstract class HttpServletRequestStub implements HttpServletRequest { private int localPort = LOCAL_PORT; private String contentType = DEFAULT_CONTENT_TYPE; private String contents; - private ServletInputStream inputStream; + private List cookies = null; private String contextPath; + private ServletInputStream inputStream; private String servletPath = ""; private HttpSessionStub session; private boolean secure; @@ -113,6 +117,11 @@ public String getContentType() { return contentType; } + @Override + public Cookie[] getCookies() { + return cookies == null ? null : cookies.toArray(new Cookie[0]); + } + @Override public String getProtocol() { return "HTTP/1.1"; @@ -193,6 +202,11 @@ public boolean hasInvalidatedSession() { return session != null && !session.valid; } + public void addCookie(String name, String value) { + if (cookies == null) cookies = new ArrayList<>(); + cookies.add(new Cookie(name, value)); + } + static abstract class HttpSessionStub implements HttpSession { private boolean valid = true; diff --git a/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/HelidonInvocationContextTest.java b/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/HelidonInvocationContextTest.java index 80d8a79c..0d1fb59a 100644 --- a/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/HelidonInvocationContextTest.java +++ b/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/HelidonInvocationContextTest.java @@ -1,4 +1,4 @@ -// Copyright (c) 2021, 2022, Oracle and/or its affiliates. +// Copyright (c) 2021, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter.sidecar; @@ -107,11 +107,11 @@ public RequestHeaders headers() { abstract static class RequestHeadersStub implements RequestHeaders { - private final Map headers = new HashMap<>(); + private final Map> headers = new HashMap<>(); private MediaType contentType; void addHeader(String name, String value) { - headers.put(name, value); + all(name).add(value); } void setContentType(MediaType contentType) { @@ -120,7 +120,12 @@ void setContentType(MediaType contentType) { @Override public Optional first(String name) { - return Optional.ofNullable(headers.get(name)); + return Optional.ofNullable(headers.get(name)).map(l->l.get(0)); + } + + @Override + public List all(String name) { + return headers.computeIfAbsent(name, k -> new ArrayList<>()); } @Override diff --git a/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/MetricsServiceTest.java b/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/MetricsServiceTest.java index c4296ba4..e3769811 100644 --- a/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/MetricsServiceTest.java +++ b/wls-exporter-sidecar/src/test/java/com/oracle/wls/exporter/sidecar/MetricsServiceTest.java @@ -1,4 +1,4 @@ -// Copyright (c) 2021, 2022, Oracle and/or its affiliates. +// Copyright (c) 2021, 2023, Oracle and/or its affiliates. // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. package com.oracle.wls.exporter.sidecar; @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import static com.meterware.simplestub.Stub.createStub; +import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_CHALLENGE_HEADER; import static com.oracle.wls.exporter.WebAppConstants.AUTHENTICATION_HEADER; import static com.oracle.wls.exporter.domain.QueryType.RUNTIME_URL_PATTERN; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; @@ -151,11 +152,11 @@ void whenServerSends401StatusOnGet_returnToClient() throws Exception { final TestResponse metricsResponse = getMetricsResponse(); assertThat(metricsResponse.status().code(), equalTo(HTTP_UNAUTHORIZED)); - assertThat(getAuthenticationHeader(metricsResponse), equalTo("Basic realm=\"Test-Realm\"")); + assertThat(getAuthenticationChallengeHeader(metricsResponse), equalTo("Basic realm=\"Test-Realm\"")); } - private String getAuthenticationHeader(TestResponse metricsResponse) { - return metricsResponse.headers().first("WWW-Authenticate").orElse(null); + private String getAuthenticationChallengeHeader(TestResponse metricsResponse) { + return metricsResponse.headers().first(AUTHENTICATION_CHALLENGE_HEADER).orElse(null); } @Test diff --git a/wls-exporter-war/src/main/webapp/WEB-INF/weblogic.xml b/wls-exporter-war/src/main/webapp/WEB-INF/weblogic.xml new file mode 100644 index 00000000..0b6ca104 --- /dev/null +++ b/wls-exporter-war/src/main/webapp/WEB-INF/weblogic.xml @@ -0,0 +1,10 @@ + + + + false + + +