Repository: brooklyn-server Updated Branches: refs/heads/master 9e53af950 -> d5cd336fb
Adds HttpFeed.preemptiveBasicAuth Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/6b9c6ac1 Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/6b9c6ac1 Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/6b9c6ac1 Branch: refs/heads/master Commit: 6b9c6ac172a5e1ddfbc07f624324a9a589a7d7bc Parents: 34b4c0d Author: Aled Sage <[email protected]> Authored: Thu Aug 3 17:38:41 2017 +0100 Committer: Aled Sage <[email protected]> Committed: Thu Aug 3 17:44:22 2017 +0100 ---------------------------------------------------------------------- .../core/sensor/http/HttpRequestSensor.java | 12 ++- .../org/apache/brooklyn/feed/http/HttpFeed.java | 34 +++++++- .../core/sensor/http/HttpRequestSensorTest.java | 40 ++++++++- .../feed/http/HttpFeedIntegrationTest.java | 10 +++ .../apache/brooklyn/feed/http/HttpFeedTest.java | 89 +++++++++++++++++++- .../test/http/RecordingHttpRequestHandler.java | 78 +++++++++++++++++ 6 files changed, 251 insertions(+), 12 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6b9c6ac1/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java index 22e24f1..107d9a4 100644 --- a/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java @@ -56,8 +56,13 @@ public final class HttpRequestSensor<T> extends AddSensor<T> { public static final ConfigKey<String> JSON_PATH = ConfigKeys.newStringConfigKey("jsonPath", "JSON path to select in HTTP response; default $", "$"); public static final ConfigKey<String> USERNAME = ConfigKeys.newStringConfigKey("username", "Username for HTTP request, if required"); public static final ConfigKey<String> PASSWORD = ConfigKeys.newStringConfigKey("password", "Password for HTTP request, if required"); - public static final ConfigKey<Map<String, String>> HEADERS = new MapConfigKey(String.class, "headers"); - + public static final ConfigKey<Map<String, String>> HEADERS = new MapConfigKey<>(String.class, "headers"); + + public static final ConfigKey<Boolean> PREEMPTIVE_BASIC_AUTH = ConfigKeys.newBooleanConfigKey( + "preemptiveBasicAuth", + "Whether to pre-emptively including a basic-auth header of the username:password (rather than waiting for a challenge)", + Boolean.FALSE); + public HttpRequestSensor(final ConfigBag params) { super(params); } @@ -85,7 +90,7 @@ public final class HttpRequestSensor<T> extends AddSensor<T> { final String username = EntityInitializers.resolve(allConfig, USERNAME); final String password = EntityInitializers.resolve(allConfig, PASSWORD); final Map<String, String> headers = EntityInitializers.resolve(allConfig, HEADERS); - + final Boolean preemptiveBasicAuth = EntityInitializers.resolve(allConfig, PREEMPTIVE_BASIC_AUTH); HttpPollConfig<T> pollConfig = new HttpPollConfig<T>(sensor) .checkSuccess(HttpValueFunctions.responseCodeEquals(200)) @@ -96,6 +101,7 @@ public final class HttpRequestSensor<T> extends AddSensor<T> { HttpFeed.Builder httpRequestBuilder = HttpFeed.builder().entity(entity) .baseUri(uri) .credentialsIfNotNull(username, password) + .preemptiveBasicAuth(Boolean.TRUE.equals(preemptiveBasicAuth)) .poll(pollConfig); if (headers != null) { http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6b9c6ac1/core/src/main/java/org/apache/brooklyn/feed/http/HttpFeed.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/brooklyn/feed/http/HttpFeed.java b/core/src/main/java/org/apache/brooklyn/feed/http/HttpFeed.java index 36d19c2..37fb32f 100644 --- a/core/src/main/java/org/apache/brooklyn/feed/http/HttpFeed.java +++ b/core/src/main/java/org/apache/brooklyn/feed/http/HttpFeed.java @@ -24,6 +24,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.List; import java.util.Map; @@ -47,11 +48,11 @@ import org.apache.brooklyn.core.location.Machines; import org.apache.brooklyn.util.executor.HttpExecutorFactory; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.http.HttpToolResponse; -import org.apache.brooklyn.util.http.executor.UsernamePassword; import org.apache.brooklyn.util.http.executor.HttpConfig; import org.apache.brooklyn.util.http.executor.HttpExecutor; import org.apache.brooklyn.util.http.executor.HttpRequest; import org.apache.brooklyn.util.http.executor.HttpResponse; +import org.apache.brooklyn.util.http.executor.UsernamePassword; import org.apache.brooklyn.util.http.executor.apacheclient.HttpExecutorImpl; import org.apache.brooklyn.util.stream.Streams; import org.apache.brooklyn.util.time.Duration; @@ -71,6 +72,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; +import com.google.common.io.BaseEncoding; import com.google.common.io.ByteStreams; import com.google.common.reflect.TypeToken; @@ -144,6 +146,7 @@ public class HttpFeed extends AbstractFeed { private Credentials credentials; private String uniqueTag; private HttpExecutor httpExecutor; + private Boolean preemptiveBasicAuth; private volatile boolean built; public Builder entity(Entity val) { @@ -220,6 +223,10 @@ public class HttpFeed extends AbstractFeed { } return this; } + public Builder preemptiveBasicAuth(Boolean val) { + this.preemptiveBasicAuth = val; + return this; + } public Builder uniqueTag(String uniqueTag) { this.uniqueTag = uniqueTag; return this; @@ -228,6 +235,25 @@ public class HttpFeed extends AbstractFeed { this.httpExecutor = val; return this; } + public Map<String, String> buildBaseHeaders() { + if (Boolean.TRUE.equals(preemptiveBasicAuth)) { + Credentials creds = credentials; + if (creds == null) { + throw new IllegalArgumentException("Must not enable preemptiveBasicAuth when there are no credentials, in feed for "+baseUri); + } + String username = checkNotNull(creds.getUserPrincipal().getName(), "username"); + String password = creds.getPassword(); + String toencode = username + (password == null ? "" : ":"+password); + String headerVal = "Basic " + BaseEncoding.base64().encode((toencode).getBytes(StandardCharsets.UTF_8)); + + return ImmutableMap.<String,String>builder() + .put("Authorization", headerVal) + .putAll(checkNotNull(headers, "headers")) + .build(); + } else { + return ImmutableMap.copyOf(checkNotNull(headers, "headers")); + } + } public HttpFeed build() { built = true; HttpFeed result = new HttpFeed(this); @@ -297,8 +323,8 @@ public class HttpFeed extends AbstractFeed { } protected HttpFeed(Builder builder) { - setConfig(ONLY_IF_SERVICE_UP, builder.onlyIfServiceUp); - Map<String,String> baseHeaders = ImmutableMap.copyOf(checkNotNull(builder.headers, "headers")); + config().set(ONLY_IF_SERVICE_UP, builder.onlyIfServiceUp); + Map<String,String> baseHeaders = builder.buildBaseHeaders(); HttpExecutor httpExecutor; if (builder.httpExecutor != null) { @@ -344,7 +370,7 @@ public class HttpFeed extends AbstractFeed { polls.put(new HttpPollIdentifier(httpExecutor, method, baseUriProvider, headers, body, credentials, connectionTimeout, socketTimeout), configCopy); } - setConfig(POLLS, polls); + config().set(POLLS, polls); initUniqueTag(builder.uniqueTag, polls.values()); } http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6b9c6ac1/core/src/test/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensorTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensorTest.java b/core/src/test/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensorTest.java index 34b2d79..876b190 100644 --- a/core/src/test/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensorTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensorTest.java @@ -18,6 +18,8 @@ */ package org.apache.brooklyn.core.sensor.http; +import static org.testng.Assert.assertEquals; + import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.location.Location; @@ -28,15 +30,20 @@ import org.apache.brooklyn.core.entity.EntityAsserts; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.feed.http.HttpFeedTest; +import org.apache.brooklyn.test.http.RecordingHttpRequestHandler; import org.apache.brooklyn.test.http.TestHttpRequestHandler; import org.apache.brooklyn.test.http.TestHttpServer; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.time.Duration; +import org.apache.http.HttpRequest; +import org.apache.http.protocol.HttpRequestHandler; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; public class HttpRequestSensorTest { final static AttributeSensor<String> SENSOR_STRING = Sensors.newStringSensor("aString"); @@ -47,11 +54,14 @@ public class HttpRequestSensorTest { private TestHttpServer server; private String serverUrl; + private RecordingHttpRequestHandler recordingHandler; - @BeforeClass(alwaysRun=true) + @BeforeMethod(alwaysRun=true) public void setUp() throws Exception { + HttpRequestHandler handler = new TestHttpRequestHandler().header("Content-Type", "application/json").response("{\"myKey\":\"myValue\"}"); + recordingHandler = new RecordingHttpRequestHandler(handler); server = new TestHttpServer() - .handler("/myKey/myValue", new TestHttpRequestHandler().header("Content-Type", "application/json").response("{\"myKey\":\"myValue\"}")) + .handler("/myKey/myValue", recordingHandler) .start(); serverUrl = server.getUrl(); @@ -67,8 +77,8 @@ public class HttpRequestSensorTest { server.stop(); } - @SuppressWarnings("deprecation") @Test + @SuppressWarnings("deprecation") public void testHttpSensor() throws Exception { HttpRequestSensor<Integer> sensor = new HttpRequestSensor<Integer>(ConfigBag.newInstance() .configure(HttpRequestSensor.SENSOR_PERIOD, Duration.millis(100)) @@ -82,4 +92,26 @@ public class HttpRequestSensorTest { EntityAsserts.assertAttributeEqualsEventually(entity, SENSOR_STRING, "myValue"); } + @Test + @SuppressWarnings("deprecation") + public void testPreemptiveBasicAuth() throws Exception { + HttpRequestSensor<Integer> sensor = new HttpRequestSensor<Integer>(ConfigBag.newInstance() + .configure(HttpRequestSensor.PREEMPTIVE_BASIC_AUTH, true) + .configure(HttpRequestSensor.USERNAME, "myuser") + .configure(HttpRequestSensor.PASSWORD, "mypass") + .configure(HttpRequestSensor.SENSOR_PERIOD, Duration.minutes(1)) + .configure(HttpRequestSensor.SENSOR_NAME, SENSOR_STRING.getName()) + .configure(HttpRequestSensor.SENSOR_TYPE, TARGET_TYPE) + .configure(HttpRequestSensor.JSON_PATH, "$.myKey") + .configure(HttpRequestSensor.SENSOR_URI, serverUrl + "/myKey/myValue")); + sensor.apply((org.apache.brooklyn.api.entity.EntityLocal)entity); + entity.sensors().set(Attributes.SERVICE_UP, true); + + EntityAsserts.assertAttributeEqualsEventually(entity, SENSOR_STRING, "myValue"); + + HttpRequest req = Iterables.getFirst(recordingHandler.getRequests(), null); + String headerVal = req.getFirstHeader("Authorization").getValue(); + String expectedVal = HttpFeedTest.getBasicAuthHeaderVal("myuser", "mypass"); + assertEquals(headerVal, expectedVal); + } } http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6b9c6ac1/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedIntegrationTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedIntegrationTest.java index 305fc75..e0fe384 100644 --- a/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedIntegrationTest.java +++ b/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedIntegrationTest.java @@ -99,6 +99,15 @@ public class HttpFeedIntegrationTest extends BrooklynAppUnitTestSupport { @Test(groups = {"Integration"}) public void testPollsAndParsesHttpGetResponseWithBasicAuthentication() throws Exception { + runPollsAndParsesHttpGetResponseWithBasicAuthentication(false); + } + + @Test(groups = {"Integration"}) + public void testPollsAndParsesHttpGetResponseWithPreemptiveBasicAuthentication() throws Exception { + runPollsAndParsesHttpGetResponseWithBasicAuthentication(true); + } + + protected void runPollsAndParsesHttpGetResponseWithBasicAuthentication(boolean preemptiveBasicAuth) throws Exception { final String username = "brooklyn"; final String password = "hunter2"; httpService = new HttpService(PortRanges.fromString("9000+")) @@ -111,6 +120,7 @@ public class HttpFeedIntegrationTest extends BrooklynAppUnitTestSupport { .entity(entity) .baseUri(baseUrl) .credentials(username, password) + .preemptiveBasicAuth(preemptiveBasicAuth) .poll(new HttpPollConfig<Integer>(SENSOR_INT) .period(100) .onSuccess(HttpValueFunctions.responseCode())) http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6b9c6ac1/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedTest.java b/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedTest.java index f2c8431..08abbe6 100644 --- a/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedTest.java +++ b/core/src/test/java/org/apache/brooklyn/feed/http/HttpFeedTest.java @@ -22,6 +22,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.Callable; @@ -30,6 +31,7 @@ import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.EntityAsserts; import org.apache.brooklyn.core.entity.EntityFunctions; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.entity.EntityInternal.FeedSupport; @@ -42,8 +44,8 @@ import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.http.BetterMockWebServer; -import org.apache.brooklyn.util.http.HttpToolResponse; import org.apache.brooklyn.util.guava.Functionals; +import org.apache.brooklyn.util.http.HttpToolResponse; import org.apache.brooklyn.util.net.Networking; import org.apache.brooklyn.util.time.Duration; import org.slf4j.Logger; @@ -59,7 +61,9 @@ import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.common.io.BaseEncoding; import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; public class HttpFeedTest extends BrooklynAppUnitTestSupport { @@ -360,7 +364,90 @@ public class HttpFeedTest extends BrooklynAppUnitTestSupport { server.shutdown(); } + + @Test + public void testPreemptiveBasicAuth() throws Exception { + final String username = "brooklyn"; + final String password = "hunter2"; + + feed = HttpFeed.builder() + .entity(entity) + .baseUrl(server.getUrl("/")) + .credentials(username, password) + .preemptiveBasicAuth(true) + .poll(new HttpPollConfig<Integer>(SENSOR_INT) + .period(100) + .onSuccess(HttpValueFunctions.responseCode()) + .onException(Functions.constant(-1))) + .build(); + + EntityAsserts.assertAttributeEqualsEventually(entity, SENSOR_INT, 200); + RecordedRequest req = server.takeRequest(); + String headerVal = req.getHeader("Authorization"); + String expectedVal = getBasicAuthHeaderVal(username, password); + assertEquals(headerVal, expectedVal); + } + + @Test + public void testPreemptiveBasicAuthFailsIfNoCredentials() throws Exception { + try { + feed = HttpFeed.builder() + .entity(entity) + .baseUrl(new URL("http://shouldNeverBeCalled.org")) + .preemptiveBasicAuth(true) + .poll(new HttpPollConfig<Integer>(SENSOR_INT) + .period(100) + .onSuccess(HttpValueFunctions.responseCode()) + .onException(Functions.constant(-1))) + .build(); + Asserts.shouldHaveFailedPreviously(); + } catch (IllegalArgumentException e) { + Asserts.expectedFailureContains(e, "Must not enable preemptiveBasicAuth when there are no credentials"); + } + } + // Expected behaviour of o.a.http.client is that it first sends the request without credentials, + // and then when given a challenge for basic-auth it re-sends the request with the basic-auth header. + @Test + public void testNonPreemptiveBasicAuth() throws Exception { + final String username = "brooklyn"; + final String password = "hunter2"; + + if (server != null) server.shutdown(); + server = BetterMockWebServer.newInstanceLocalhost(); + server.enqueue(new MockResponse() + .setResponseCode(401) + .addHeader("WWW-Authenticate", "Basic")); + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("Hello World")); + server.play(); + + feed = HttpFeed.builder() + .entity(entity) + .baseUrl(server.getUrl("/")) + .credentials(username, password) + .poll(new HttpPollConfig<Integer>(SENSOR_INT) + .period(Duration.ONE_MINUTE) // so only dealing with first request + .onSuccess(HttpValueFunctions.responseCode()) + .onException(Functions.constant(-1))) + .build(); + + EntityAsserts.assertAttributeEqualsEventually(entity, SENSOR_INT, 200); + RecordedRequest req = server.takeRequest(); + assertEquals(req.getHeader("Authorization"), null); + + RecordedRequest req2 = server.takeRequest(); + String headerVal = req2.getHeader("Authorization"); + String expected = getBasicAuthHeaderVal(username, password); + assertEquals(headerVal, expected); + } + + public static String getBasicAuthHeaderVal(String username, String password) { + String toencode = username + (password == null ? "" : ":"+password); + return "Basic " + BaseEncoding.base64().encode((toencode).getBytes(StandardCharsets.UTF_8)); + } + private void newMultiFeed(URL baseUrl) { feed = HttpFeed.builder() .entity(entity) http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6b9c6ac1/utils/common/src/main/java/org/apache/brooklyn/test/http/RecordingHttpRequestHandler.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/test/http/RecordingHttpRequestHandler.java b/utils/common/src/main/java/org/apache/brooklyn/test/http/RecordingHttpRequestHandler.java new file mode 100644 index 0000000..5b9ced4 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/test/http/RecordingHttpRequestHandler.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.test.http; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.IOException; +import java.util.List; + +import org.apache.brooklyn.test.Asserts; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpRequestHandler; +import org.testng.Assert; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +public class RecordingHttpRequestHandler implements HttpRequestHandler { + private final HttpRequestHandler delegate; + + private final List<HttpRequest> requests = Lists.newCopyOnWriteArrayList(); + + public RecordingHttpRequestHandler(HttpRequestHandler delegate) { + this.delegate = checkNotNull(delegate, "delegate"); + } + + @Override + public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { + requests.add(request); + delegate.handle(request, response, context); + } + + public void assertHasRequest(Predicate<? super HttpRequest> filter) { + for (HttpRequest req : requests) { + if (filter.apply(req)) { + return; + } + } + Assert.fail("No request matching filter "+ filter); + } + + public void assertHasRequestEventually(Predicate<? super HttpRequest> filter) { + Asserts.succeedsEventually(new Runnable() { + @Override + public void run() { + assertHasRequest(filter); + }}); + } + + public List<HttpRequest> getRequests(Predicate<? super HttpRequest> filter) { + return ImmutableList.copyOf(Iterables.filter(requests, filter)); + } + + public List<HttpRequest> getRequests() { + return ImmutableList.copyOf(requests); + } +}
