diff --git a/apis/cloudstack/src/main/java/org/jclouds/cloudstack/filters/QuerySigner.java b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/filters/QuerySigner.java index 8d6cd42348..cb806ee9b1 100644 --- a/apis/cloudstack/src/main/java/org/jclouds/cloudstack/filters/QuerySigner.java +++ b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/filters/QuerySigner.java @@ -25,18 +25,19 @@ import static com.google.common.io.ByteStreams.readBytes; import static org.jclouds.Constants.LOGGER_SIGNATURE; import static org.jclouds.crypto.Macs.asByteProcessor; import static org.jclouds.http.Uris.uriBuilder; -import static org.jclouds.http.utils.Queries.encodeQueryLine; import static org.jclouds.http.utils.Queries.queryParser; import static org.jclouds.util.Strings2.toInputStream; import java.io.IOException; import java.security.InvalidKeyException; +import java.util.Map; import javax.annotation.Resource; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; +import com.google.common.base.Joiner; import org.jclouds.crypto.Crypto; import org.jclouds.domain.Credentials; import org.jclouds.http.HttpException; @@ -46,12 +47,13 @@ import org.jclouds.http.internal.SignatureWire; import org.jclouds.location.Provider; import org.jclouds.logging.Logger; import org.jclouds.rest.RequestSigner; +import org.jclouds.util.Strings2; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Multimap; -import com.google.common.collect.TreeMultimap; import com.google.common.io.ByteProcessor; /** @@ -116,10 +118,12 @@ public class QuerySigner implements AuthenticationFilter, RequestSigner { @VisibleForTesting public String createStringToSign(HttpRequest request, Multimap decodedParams) { utils.logRequest(signatureLog, request, ">>"); - // like aws, percent encode the canonicalized string without skipping '/' and '?' - String queryLine = encodeQueryLine(TreeMultimap.create(decodedParams), ImmutableList. of()); + // encode each parameter value first, + ImmutableSortedSet.Builder builder = ImmutableSortedSet.naturalOrder(); + for (Map.Entry entry : decodedParams.entries()) + builder.add(entry.getKey() + "=" + Strings2.urlEncode(entry.getValue())); // then, lower case the entire query string - String stringToSign = queryLine.toLowerCase(); + String stringToSign = Joiner.on('&').join(builder.build()).toLowerCase(); if (signatureWire.enabled()) signatureWire.output(stringToSign); diff --git a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/filters/QuerySignerTest.java b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/filters/QuerySignerTest.java index 87f2da01e0..05d80e2dc7 100644 --- a/apis/cloudstack/src/test/java/org/jclouds/cloudstack/filters/QuerySignerTest.java +++ b/apis/cloudstack/src/test/java/org/jclouds/cloudstack/filters/QuerySignerTest.java @@ -61,6 +61,18 @@ public class QuerySignerTest { "apikey=apikey&command=listzones"); } + @Test + void testCreateStringToSignWithBrackets() { + // This test asserts that key *names* are not URL-encoded - only values + // should be encoded, according to "CloudStack API Developer’s Guide". + QuerySigner filter = INJECTOR.getInstance(QuerySigner.class); + + assertEquals( + filter.createStringToSign(HttpRequest.builder().method("GET") + .endpoint("http://localhost:8080/client/api?command=deployVirtualMachine&iptonetworklist[0].ip=127.0.0.1&iptonetworklist[0].networkid=1").build()), + "apikey=apikey&command=deployvirtualmachine&iptonetworklist[0].ip=127.0.0.1&iptonetworklist[0].networkid=1"); + } + @Test void testFilter() { QuerySigner filter = INJECTOR.getInstance(QuerySigner.class);