Updated Branches: refs/heads/master cc1e2ae4d -> fd3f8cd01
JCLOUDS-308. Add Temporary Url Support to openstack-swift Project: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/commit/fd3f8cd0 Tree: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/tree/fd3f8cd0 Diff: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/diff/fd3f8cd0 Branch: refs/heads/master Commit: fd3f8cd01991363db91f90bb7f22133af1988061 Parents: cc1e2ae Author: Adrian Cole <[email protected]> Authored: Sun Sep 29 15:42:46 2013 -0700 Committer: Adrian Cole <[email protected]> Committed: Sun Sep 29 16:27:41 2013 -0700 ---------------------------------------------------------------------- .../openstack/swift/v1/TemporaryUrlSigner.java | 92 ++++++++++++++++++++ .../openstack/swift/v1/domain/Account.java | 5 ++ .../openstack/swift/v1/features/AccountApi.java | 20 +++++ .../swift/v1/TemporaryUrlSignerLiveTest.java | 91 +++++++++++++++++++ .../swift/v1/TemporaryUrlSignerMockTest.java | 75 ++++++++++++++++ .../swift/v1/features/AccountApiMockTest.java | 20 +++++ 6 files changed, 303 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/fd3f8cd0/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/TemporaryUrlSigner.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/TemporaryUrlSigner.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/TemporaryUrlSigner.java new file mode 100644 index 0000000..35f2b12 --- /dev/null +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/TemporaryUrlSigner.java @@ -0,0 +1,92 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed 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.swift.v1; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Suppliers.memoizeWithExpiration; +import static com.google.common.base.Throwables.propagate; +import static com.google.common.io.BaseEncoding.base16; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.jclouds.openstack.swift.v1.features.AccountApi; + +import com.google.common.base.Supplier; + +/** + * Use this utility to create temporary urls. + * + * @see <a + * href="http://docs.openstack.org/trunk/config-reference/content/object-storage-tempurl.html">Temporary + * URL Documentation</a> + */ +public class TemporaryUrlSigner { + + public static TemporaryUrlSigner checkApiEvery(final AccountApi api, long seconds) { + Supplier<String> keySupplier = memoizeWithExpiration(new TemporaryUrlKeyFromAccount(api), seconds, SECONDS); + return new TemporaryUrlSigner(keySupplier); + } + + private final Supplier<String> keySupplier; + + TemporaryUrlSigner(Supplier<String> keySupplier) { + this.keySupplier = keySupplier; + } + + public String sign(String method, String path, long expirationTimestampSeconds) { + checkNotNull(method, "method"); + checkNotNull(path, "path"); + checkArgument(expirationTimestampSeconds > 0, "expirationTimestamp must be a unix epoch timestamp"); + String hmacBody = format("%s\n%s\n%s", method, expirationTimestampSeconds, path); + return base16().lowerCase().encode(hmacSHA1(hmacBody)); + } + + byte[] hmacSHA1(String data) { + try { + String key = keySupplier.get(); + checkState(key != null, "%s returned a null temporaryUrlKey!", keySupplier); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec(key.getBytes(UTF_8), "HmacSHA1")); + return mac.doFinal(data.getBytes(UTF_8)); + } catch (Exception e) { + throw propagate(e); + } + } + + static class TemporaryUrlKeyFromAccount implements Supplier<String> { + private final AccountApi api; + + private TemporaryUrlKeyFromAccount(AccountApi api) { + this.api = checkNotNull(api, "accountApi"); + } + + @Override + public String get() { + return api.get().temporaryUrlKey().orNull(); + } + + @Override + public String toString() { + return format("get().temporaryUrlKey() using %s", api); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/fd3f8cd0/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java index 304f329..a3857f0 100644 --- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/Account.java @@ -25,6 +25,7 @@ import java.util.Map.Entry; import com.google.common.base.Objects; import com.google.common.base.Objects.ToStringHelper; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; /** @@ -59,6 +60,10 @@ public class Account { return bytesUsed; } + public Optional<String> temporaryUrlKey() { + return Optional.fromNullable(metadata.get("temp-url-key")); + } + /** * In current swift implementations, headers keys are lower-cased. This means * characters such as turkish will probably not work out well. http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/fd3f8cd0/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java index 5e0bc6d..9736112 100644 --- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java +++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/AccountApi.java @@ -23,6 +23,7 @@ import java.util.Map; import javax.inject.Named; import javax.ws.rs.Consumes; import javax.ws.rs.HEAD; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -88,6 +89,25 @@ public interface AccountApi { boolean updateMetadata(@BinderParam(BindAccountMetadataToHeaders.class) Map<String, String> metadata); /** + * Replaces the {@link Account#temporaryUrlKey()}. + * + * @param metadata + * the Account metadata to create or update. + * + * @see <a + * href="http://docs.openstack.org/trunk/config-reference/content/object-storage-tempurl.html"> + * Temporary URL Documentation</a> + * + * @return <code>true</code> if the Temporary URL Key was successfully created + * or updated, false if not. + */ + @Named("UpdateAccountMetadata") + @POST + @Fallback(FalseOnNotFoundOr404.class) + @Path("/") + boolean updateTemporaryUrlKey(@HeaderParam("X-Account-Meta-Temp-URL-Key") String temporaryUrlKey); + + /** * Deletes Account metadata. * * @param metadata http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/fd3f8cd0/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerLiveTest.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerLiveTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerLiveTest.java new file mode 100644 index 0000000..f2ba78f --- /dev/null +++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerLiveTest.java @@ -0,0 +1,91 @@ +/* + * 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.jclouds.openstack.swift.v1; + +import static java.lang.String.format; +import static org.jclouds.io.Payloads.newStringPayload; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.UUID; + +import org.jclouds.openstack.swift.v1.domain.SwiftObject; +import org.jclouds.openstack.swift.v1.internal.BaseSwiftApiLiveTest; +import org.jclouds.openstack.swift.v1.options.CreateContainerOptions; +import org.jclouds.util.Strings2; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; + +@Test(groups = "live", testName = "TemporaryUrlSignerLiveTest") +public class TemporaryUrlSignerLiveTest extends BaseSwiftApiLiveTest { + + private String name = getClass().getSimpleName(); + private String containerName = getClass().getSimpleName() + "Container"; + + public void signForPublicAccess() throws Exception { + for (String regionId : api.configuredRegions()) { + SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName).head(name); + + long expires = System.currentTimeMillis() / 1000 + 5; + String signature = TemporaryUrlSigner.checkApiEvery(api.accountApiInRegion(regionId), 5) // + .sign("GET", object.uri().getPath(), expires); + + URI signed = URI.create(format("%s?temp_url_sig=%s&temp_url_expires=%s", object.uri(), signature, expires)); + + InputStream publicStream = signed.toURL().openStream(); + assertEquals(Strings2.toStringAndClose(publicStream), "swifty"); + + // let it expire + Thread.sleep(5000); + try { + signed.toURL().openStream(); + fail("should have expired!"); + } catch (IOException e) { + } + } + } + + @Override + @BeforeClass(groups = "live") + public void setup() { + super.setup(); + String key = UUID.randomUUID().toString(); + for (String regionId : api.configuredRegions()) { + api.accountApiInRegion(regionId).updateTemporaryUrlKey(key); + api.containerApiInRegion(regionId).createIfAbsent(containerName, new CreateContainerOptions()); + api.objectApiInRegionForContainer(regionId, containerName) // + .replace(name, newStringPayload("swifty"), ImmutableMap.<String, String> of()); + } + } + + @AfterMethod + @AfterClass(groups = "live") + public void tearDown() { + for (String regionId : api.configuredRegions()) { + api.objectApiInRegionForContainer(regionId, containerName).delete(name); + api.containerApiInRegion(regionId).deleteIfEmpty(containerName); + } + super.tearDown(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/fd3f8cd0/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerMockTest.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerMockTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerMockTest.java new file mode 100644 index 0000000..c3711a8 --- /dev/null +++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TemporaryUrlSignerMockTest.java @@ -0,0 +1,75 @@ +/* + * 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.jclouds.openstack.swift.v1; + +import static org.jclouds.openstack.swift.v1.features.AccountApiMockTest.accountResponse; +import static org.testng.Assert.assertEquals; + +import org.jclouds.openstack.swift.v1.internal.BaseSwiftMockTest; +import org.testng.annotations.Test; + +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; + +@Test +public class TemporaryUrlSignerMockTest extends BaseSwiftMockTest { + + @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "accountApi") + public void whenAccountApiIsNull() { + TemporaryUrlSigner.checkApiEvery(null, 10000); + } + + public void whenAccountApiHasKey() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(accountResponse().addHeader("X-Account-Meta-Temp-URL-Key", "mykey")); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + String signature = TemporaryUrlSigner.checkApiEvery(api.accountApiInRegion("DFW"), 10000) + .sign("GET", "/v1/AUTH_account/container/object", 1323479485l); + + assertEquals(signature, "d9fc2067e52b06598421664cf6610bfc8fc431f6"); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + assertEquals(server.takeRequest().getRequestLine(), + "HEAD /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/ HTTP/1.1"); + } finally { + server.shutdown(); + } + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*returned a null temporaryUrlKey!") + public void whenAccountApiDoesntHaveKey() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(accountResponse()); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + TemporaryUrlSigner.checkApiEvery(api.accountApiInRegion("DFW"), 10000) + .sign("GET","/v1/AUTH_account/container/object", 1323479485l); + } finally { + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + assertEquals(server.takeRequest().getRequestLine(), + "HEAD /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/ HTTP/1.1"); + server.shutdown(); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/fd3f8cd0/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/AccountApiMockTest.java ---------------------------------------------------------------------- diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/AccountApiMockTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/AccountApiMockTest.java index c2a37c3..d4f7eda 100644 --- a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/AccountApiMockTest.java +++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/AccountApiMockTest.java @@ -90,6 +90,26 @@ public class AccountApiMockTest extends BaseSwiftMockTest { } } + public void updateTemporaryUrlKey() throws Exception { + MockWebServer server = mockSwiftServer(); + server.enqueue(new MockResponse().setBody(access)); + server.enqueue(accountResponse()); + + try { + SwiftApi api = swiftApi(server.getUrl("/").toString()); + assertTrue(api.accountApiInRegion("DFW").updateTemporaryUrlKey("foobar")); + + assertEquals(server.getRequestCount(), 2); + assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1"); + RecordedRequest replaceRequest = server.takeRequest(); + assertEquals(replaceRequest.getRequestLine(), + "POST /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/ HTTP/1.1"); + assertEquals(replaceRequest.getHeader("X-Account-Meta-Temp-URL-Key"), "foobar"); + } finally { + server.shutdown(); + } + } + public void deleteMetadata() throws Exception { MockWebServer server = mockSwiftServer(); server.enqueue(new MockResponse().setBody(access));
