From 2d0db63f519768ff27f5550afae7314f3da299fe Mon Sep 17 00:00:00 2001 From: Andrew Donald Kennedy Date: Tue, 10 Jan 2012 09:44:01 +0000 Subject: [PATCH] Issue 731: Add RetryOnRenew handler to renew expired token --- .../config/CloudServersRestClientModule.java | 7 + .../handlers/RetryOnRenewExpectTest.java | 173 ++++++++++++++++++ .../config/OpenStackAuthenticationModule.java | 41 ++++- .../openstack/handlers/RetryOnRenew.java | 98 ++++++++++ .../openstack/handlers/RetryOnRenewTest.java | 73 ++++++++ 5 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 apis/cloudservers/src/test/java/org/jclouds/cloudservers/handlers/RetryOnRenewExpectTest.java create mode 100644 common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java create mode 100644 common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java diff --git a/apis/cloudservers/src/main/java/org/jclouds/cloudservers/config/CloudServersRestClientModule.java b/apis/cloudservers/src/main/java/org/jclouds/cloudservers/config/CloudServersRestClientModule.java index c643ce33a9..7340bb7afa 100644 --- a/apis/cloudservers/src/main/java/org/jclouds/cloudservers/config/CloudServersRestClientModule.java +++ b/apis/cloudservers/src/main/java/org/jclouds/cloudservers/config/CloudServersRestClientModule.java @@ -27,6 +27,7 @@ import org.jclouds.cloudservers.CloudServersClient; import org.jclouds.cloudservers.ServerManagement; import org.jclouds.cloudservers.handlers.ParseCloudServersErrorFromHttpResponse; import org.jclouds.http.HttpErrorHandler; +import org.jclouds.http.HttpRetryHandler; import org.jclouds.http.RequiresHttp; import org.jclouds.http.annotation.ClientError; import org.jclouds.http.annotation.Redirection; @@ -35,6 +36,7 @@ import org.jclouds.json.config.GsonModule.DateAdapter; import org.jclouds.json.config.GsonModule.Iso8601DateAdapter; import org.jclouds.openstack.OpenStackAuthAsyncClient.AuthenticationResponse; import org.jclouds.openstack.config.OpenStackAuthenticationModule; +import org.jclouds.openstack.handlers.RetryOnRenew; import org.jclouds.openstack.reference.AuthHeaders; import org.jclouds.rest.ConfiguresRestClient; import org.jclouds.rest.config.RestClientModule; @@ -74,6 +76,11 @@ public class CloudServersRestClientModule extends RestClientModule builder() + .put("X-Auth-User", "identity") + .put("X-Auth-Key", "credential") + .put("Accept", "*/*").build()).build(); + + + HttpResponse responseWithUrls = HttpResponse.builder().statusCode(204).message("HTTP/1.1 204 No Content") + .headers(ImmutableMultimap.builder() + .put("Server", "Apache/2.2.3 (Red Hat)") + .put("vary", "X-Auth-Token,X-Auth-Key,X-Storage-User,X-Storage-Pass") + .put("X-Storage-Url", "https://storage101.dfw1.clouddrive.com/v1/MossoCloudFS_dc1f419c-5059-4c87-a389-3f2e33a77b22") + .put("Cache-Control", "s-maxage=86399") + .put("Content-Type", "text/xml") + .put("Date", "Tue, 10 Jan 2012 22:08:47 GMT") + .put("X-Auth-Token", authToken) + .put("X-Server-Management-Url","https://servers.api.rackspacecloud.com/v1.0/413274") + .put("X-Storage-Token", authToken) + .put("Connection", "Keep-Alive") + .put("X-CDN-Management-Url", "https://cdn1.clouddrive.com/v1/MossoCloudFS_dc1f419c-5059-4c87-a389-3f2e33a77b22") + .put("Content-Length", "0") + .build()).build(); + + HttpResponse responseWithUrls2 = HttpResponse.builder().statusCode(204).message("HTTP/1.1 204 No Content") + .headers(ImmutableMultimap.builder() + .put("Server", "Apache/2.2.3 (Red Hat)") + .put("vary", "X-Auth-Token,X-Auth-Key,X-Storage-User,X-Storage-Pass") + .put("X-Storage-Url", "https://storage101.dfw1.clouddrive.com/v1/MossoCloudFS_dc1f419c-5059-4c87-a389-3f2e33a77b22") + .put("Cache-Control", "s-maxage=86399") + .put("Content-Type", "text/xml") + .put("Date", "Tue, 10 Jan 2012 22:08:47 GMT") + .put("X-Auth-Token", authToken2) + .put("X-Server-Management-Url","https://servers.api.rackspacecloud.com/v1.0/413274") + .put("X-Storage-Token", authToken2) + .put("Connection", "Keep-Alive") + .put("X-CDN-Management-Url", "https://cdn1.clouddrive.com/v1/MossoCloudFS_dc1f419c-5059-4c87-a389-3f2e33a77b22") + .put("Content-Length", "0") + .build()).build(); + + HttpRequest deleteImage = HttpRequest.builder().method("DELETE").endpoint( + URI.create("https://servers.api.rackspacecloud.com/v1.0/413274/images/11?now=1257695648897")).headers( + ImmutableMultimap. builder() + .put("X-Auth-Token", authToken).build()).build(); + + HttpResponse pleaseRenew = HttpResponse.builder().statusCode(401) + .message("HTTP/1.1 401 Unauthorized") + .payload(Payloads.newStringPayload("[{\"unauthorized\":{\"message\":\"Invalid authentication token. Please renew.\",\"code\":401}}]")) + .build(); + + HttpRequest deleteImage2 = HttpRequest.builder().method("DELETE").endpoint( + URI.create("https://servers.api.rackspacecloud.com/v1.0/413274/images/11?now=1257695648897")).headers( + ImmutableMultimap. builder() + .put("X-Auth-Token", authToken2).build()).build(); + + HttpResponse imageDeleted = HttpResponse.builder().statusCode(204).message("HTTP/1.1 204 No Content").build(); + + CloudServersClient clientWhenImageExists = orderedRequestsSendResponses(initialAuth, responseWithUrls, + deleteImage, pleaseRenew, initialAuth, responseWithUrls2, deleteImage2, imageDeleted); + + assert clientWhenImageExists.deleteImage(11); + } + + @Test(expectedExceptions=AuthorizationException.class) + public void testDoesNotReauthenticateOnFatal401() { + String authToken = "d6245d35-22a0-47c0-9770-2c5097da25fc"; + + HttpRequest initialAuth = HttpRequest.builder().method("GET").endpoint(URI.create("https://auth/v1.0")) + .headers( + ImmutableMultimap. builder() + .put("X-Auth-User", "identity") + .put("X-Auth-Key", "credential") + .put("Accept", "*/*").build()).build(); + + + HttpResponse responseWithUrls = HttpResponse.builder().statusCode(204).message("HTTP/1.1 204 No Content") + .headers(ImmutableMultimap.builder() + .put("Server", "Apache/2.2.3 (Red Hat)") + .put("vary", "X-Auth-Token,X-Auth-Key,X-Storage-User,X-Storage-Pass") + .put("X-Storage-Url", "https://storage101.dfw1.clouddrive.com/v1/MossoCloudFS_dc1f419c-5059-4c87-a389-3f2e33a77b22") + .put("Cache-Control", "s-maxage=86399") + .put("Content-Type", "text/xml") + .put("Date", "Tue, 10 Jan 2012 22:08:47 GMT") + .put("X-Auth-Token", authToken) + .put("X-Server-Management-Url","https://servers.api.rackspacecloud.com/v1.0/413274") + .put("X-Storage-Token", authToken) + .put("Connection", "Keep-Alive") + .put("X-CDN-Management-Url", "https://cdn1.clouddrive.com/v1/MossoCloudFS_dc1f419c-5059-4c87-a389-3f2e33a77b22") + .put("Content-Length", "0") + .build()).build(); + + HttpRequest deleteImage = HttpRequest.builder().method("DELETE").endpoint( + URI.create("https://servers.api.rackspacecloud.com/v1.0/413274/images/11?now=1257695648897")).headers( + ImmutableMultimap. builder() + .put("X-Auth-Token", authToken).build()).build(); + + HttpResponse unauthResponse = HttpResponse.builder().statusCode(401) + .message("HTTP/1.1 401 Unauthorized") + .payload(Payloads.newStringPayload("[{\"unauthorized\":{\"message\":\"Fatal unauthorized.\",\"code\":401}}]")) + .build(); + + CloudServersClient client = orderedRequestsSendResponses(initialAuth, responseWithUrls, + deleteImage, unauthResponse); + + client.deleteImage(11); + } + + // FIXME stack trace shows the AuthorizationException, but it's buried inside a guice TestException + @Test(enabled=false, expectedExceptions=AuthorizationException.class) + public void testDoesNotReauthenticateOnAuthentication401() { + HttpRequest initialAuth = HttpRequest.builder().method("GET").endpoint(URI.create("https://auth/v1.0")) + .headers( + ImmutableMultimap. builder() + .put("X-Auth-User", "identity") + .put("X-Auth-Key", "credential") + .put("Accept", "*/*").build()).build(); + + + HttpResponse unauthResponse = HttpResponse.builder().statusCode(401) + .message("HTTP/1.1 401 Unauthorized") + .payload(Payloads.newStringPayload("[{\"unauthorized\":{\"message\":\"A different message implying fatal.\",\"code\":401}}]")) + .build(); + + CloudServersClient client = orderedRequestsSendResponses(initialAuth, unauthResponse); + + client.deleteImage(11); + } +} diff --git a/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java b/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java index 86bad710e2..e7f180d312 100644 --- a/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java +++ b/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java @@ -40,6 +40,10 @@ import org.jclouds.rest.AsyncClientFactory; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.inject.AbstractModule; import com.google.inject.Provides; @@ -104,10 +108,41 @@ public class OpenStackAuthenticationModule extends AbstractModule { @Provides @Singleton - protected Supplier provideAuthenticationResponseCache( + public LoadingCache provideAuthenticationResponseCache2( final GetAuthenticationResponse getAuthenticationResponse) { - return Suppliers.memoizeWithExpiration(new RetryOnTimeOutExceptionSupplier( - getAuthenticationResponse), 23, TimeUnit.HOURS); + + final RetryOnTimeOutExceptionSupplier delegate = + new RetryOnTimeOutExceptionSupplier(getAuthenticationResponse); + + CacheLoader cacheLoader = new CacheLoader() { + @Override + public AuthenticationResponse load(String key) throws Exception { + return delegate.get(); + } + }; + + LoadingCache cache = CacheBuilder.newBuilder().expireAfterWrite(23, TimeUnit.HOURS) + .build(cacheLoader); + + return cache; + } + + @Provides + @Singleton + protected Supplier provideAuthenticationResponseSupplier( + final LoadingCache cache) { + return new Supplier() { + @Override + public AuthenticationResponse get() { + try { + return cache.get("key"); + } catch (UncheckedExecutionException e) { + throw Throwables.propagate(e.getCause()); + } catch (ExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + }; } @Provides diff --git a/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java b/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java new file mode 100644 index 0000000000..edb39e1a38 --- /dev/null +++ b/common/openstack/src/main/java/org/jclouds/openstack/handlers/RetryOnRenew.java @@ -0,0 +1,98 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.openstack.handlers; + +import static org.jclouds.http.HttpUtils.releasePayload; + +import java.io.IOException; + +import javax.annotation.Resource; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpRetryHandler; +import org.jclouds.logging.Logger; +import org.jclouds.openstack.OpenStackAuthAsyncClient.AuthenticationResponse; +import org.jclouds.openstack.reference.AuthHeaders; +import org.jclouds.util.Strings2; + +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Multimap; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * This will parse and set an appropriate exception on the command object. + * + * @author Adrian Cole + * + */ +@Singleton +public class RetryOnRenew implements HttpRetryHandler { + @Resource + protected Logger logger = Logger.NULL; + + // This doesn't work yet +// @Inject +// Supplier providedAuthenticationResponseCache; + + @Inject + LoadingCache authenticationResponseCache; + + @Override + public boolean shouldRetryRequest(HttpCommand command, HttpResponse response) { + boolean retry = false; // default + try { + switch (response.getStatusCode()) { + case 401: + // Do not retry on 401 from authentication request + Multimap headers = command.getCurrentRequest().getHeaders(); + if (headers != null && headers.containsKey(AuthHeaders.AUTH_USER) && headers.containsKey(AuthHeaders.AUTH_KEY) && + !headers.containsKey(AuthHeaders.AUTH_TOKEN)) { + retry = false; + } else { + String content = parsePayloadOrNull(response); + if (content != null && content.contains("lease renew")) { + // Otherwise invalidate the token cache, to force reauthentication + authenticationResponseCache.invalidateAll(); + retry = true; + } else { + retry = false; + } + } + break; + } + return retry; + + } finally { + releasePayload(response); + } + } + + String parsePayloadOrNull(HttpResponse response) { + if (response.getPayload() != null) { + try { + return Strings2.toStringAndClose(response.getPayload().getInput()); + } catch (IOException e) { + logger.warn(e, "exception reading error from response", response); + } + } + return null; + } +} diff --git a/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java b/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java new file mode 100644 index 0000000000..0aa578f3e4 --- /dev/null +++ b/common/openstack/src/test/java/org/jclouds/openstack/handlers/RetryOnRenewTest.java @@ -0,0 +1,73 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.jclouds.openstack.handlers; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.classextension.EasyMock.createMock; +import static org.easymock.classextension.EasyMock.replay; +import static org.easymock.classextension.EasyMock.verify; +import static org.testng.Assert.assertTrue; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.io.Payloads; +import org.jclouds.openstack.OpenStackAuthAsyncClient.AuthenticationResponse; +import org.testng.annotations.Test; + +import com.google.common.cache.LoadingCache; + +/** + * Tests behavior of {@code RetryOnRenew} handler + * + * @author grkvlt@apache.org + */ +@Test(groups = "unit", testName = "RetryOnRenewTest") +public class RetryOnRenewTest { + @Test + public void test401ShouldRetry() { + HttpCommand command = createMock(HttpCommand.class); + HttpRequest request = createMock(HttpRequest.class); + HttpResponse response = createMock(HttpResponse.class); + @SuppressWarnings("unchecked") + LoadingCache cache = createMock(LoadingCache.class); + + expect(command.getCurrentRequest()).andReturn(request); + + cache.invalidateAll(); + expectLastCall(); + + expect(response.getPayload()).andReturn(Payloads.newStringPayload("token expired, please renew")).anyTimes(); + expect(response.getStatusCode()).andReturn(401).atLeastOnce(); + + replay(command); + replay(response); + replay(cache); + + RetryOnRenew retry = new RetryOnRenew(); + retry.authenticationResponseCache = cache; + + assertTrue(retry.shouldRetryRequest(command, response)); + + verify(command); + verify(response); + verify(cache); + } +}