Skip to content

Commit

Permalink
fix: PostPolicyV4 classes could be improved (#442)
Browse files Browse the repository at this point in the history
* fix: PostPolicyV4 classes could be improved

* fix: javadoc

* fix: javadoc

* fix: javadoc

* test: PostPolicyV4Test unit tests

* test: PostPolicyV4Test unit tests

* test: PostPolicyV4Test unit tests

* test: PostPolicyV4Test unit tests

* fix: better URL syntax check
  • Loading branch information
dmitry-fa committed Aug 21, 2020
1 parent cdb3787 commit 8602b81
Show file tree
Hide file tree
Showing 3 changed files with 765 additions and 37 deletions.
Expand Up @@ -18,56 +18,125 @@

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
* Presigned V4 post policy.
* Presigned V4 post policy. Instances of {@code PostPolicyV4} include a URL and a map of fields
* that can be specified in an HTML form to submit a POST request to upload an object.
*
* @see <a href="https://cloud.google.com/storage/docs/xml-api/post-object">POST Object</a>
* <p>See <a href="https://cloud.google.com/storage/docs/xml-api/post-object">POST Object</a> for
* details of upload by using HTML forms.
*
* <p>See {@link Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit,
* PostPolicyV4.PostFieldsV4, PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)} for
* example of usage.
*/
public final class PostPolicyV4 {
private final String url;
private final Map<String, String> fields;

private PostPolicyV4(String url, Map<String, String> fields) {
try {
if (!new URI(url).isAbsolute()) {
throw new IllegalArgumentException(url + " is not an absolute URL");
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
PostFieldsV4.validateFields(fields);

this.url = url;
this.fields = fields;
this.fields = Collections.unmodifiableMap(fields);
}

/**
* Constructs {@code PostPolicyV4} instance of the given URL and fields map.
*
* @param url URL for the HTTP POST request
* @param fields HTML form fields
* @return constructed object
* @throws IllegalArgumentException if URL is malformed or fields are not valid
*/
public static PostPolicyV4 of(String url, Map<String, String> fields) {
return new PostPolicyV4(url, fields);
}

/** Returns the URL for the HTTP POST request */
public String getUrl() {
return url;
}

/** Returns the HTML form fields */
public Map<String, String> getFields() {
return fields;
}

/**
* Class representing which fields to specify in a V4 POST request.
* A helper class to define fields to be specified in a V4 POST request. Instance of this class
* helps to construct {@code PostPolicyV4} objects. Used in: {@link
* Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4,
* PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)}.
*
* @see <a href="https://cloud.google.com/storage/docs/xml-api/post-object#form_fields">POST
* Object Form fields</a>
*/
public static final class PostFieldsV4 {
private final Map<String, String> fieldsMap;
private static final List<String> VALID_FIELDS =
Arrays.asList(
"acl",
"bucket",
"cache-control",
"content-disposition",
"content-encoding",
"content-type",
"expires",
"file",
"key",
"policy",
"success_action_redirect",
"success_action_status",
"x-goog-algorithm",
"x-goog-credential",
"x-goog-date",
"x-goog-signature");

private static void validateFields(Map<String, String> fields) {
for (String key : fields.keySet()) {
if (!VALID_FIELDS.contains(key.toLowerCase())
&& !key.startsWith(Builder.CUSTOM_FIELD_PREFIX)) {
throw new IllegalArgumentException("Invalid key: " + key);
}
}
}

private PostFieldsV4(Builder builder) {
this.fieldsMap = builder.fieldsMap;
this(builder.fieldsMap);
}

private PostFieldsV4(Map<String, String> fields) {
this.fieldsMap = fields;
validateFields(fields);
this.fieldsMap = Collections.unmodifiableMap(fields);
}

/**
* Constructs {@code PostPolicyV4.PostFieldsV4} object of the given field map.
*
* @param fields a map of the HTML form fields
* @return constructed object
* @throws IllegalArgumentException if an unsupported field is specified
*/
public static PostFieldsV4 of(Map<String, String> fields) {
return new PostFieldsV4(fields);
}
Expand Down Expand Up @@ -112,8 +181,14 @@ public Builder setContentEncoding(String contentEncoding) {
return this;
}

/**
* @deprecated Invocation of this method has no effect, because all valid HTML form fields
* except Content-Length can use exact matching. Use {@link
* PostPolicyV4.PostConditionsV4.Builder#addContentLengthRangeCondition(int, int)} to
* specify a range for the content-length.
*/
@Deprecated
public Builder setContentLength(int contentLength) {
fieldsMap.put("content-length", "" + contentLength);
return this;
}

Expand All @@ -122,7 +197,7 @@ public Builder setContentType(String contentType) {
return this;
}

/** @deprecated use {@link #setExpires(String)} */
/** @deprecated Use {@link #setExpires(String)}. */
@Deprecated
public Builder Expires(String expires) {
return setExpires(expires);
Expand All @@ -143,15 +218,15 @@ public Builder setSuccessActionStatus(int successActionStatus) {
return this;
}

/** @deprecated use {@link #setCustomMetadataField(String, String)} */
/** @deprecated Use {@link #setCustomMetadataField(String, String)}. */
@Deprecated
public Builder AddCustomMetadataField(String field, String value) {
return setCustomMetadataField(field, value);
}

public Builder setCustomMetadataField(String field, String value) {
if (!field.startsWith(CUSTOM_FIELD_PREFIX)) {
field = CUSTOM_FIELD_PREFIX + value;
field = CUSTOM_FIELD_PREFIX + field;
}
fieldsMap.put(field, value);
return this;
Expand All @@ -160,7 +235,9 @@ public Builder setCustomMetadataField(String field, String value) {
}

/**
* Class for specifying conditions in a V4 POST Policy document.
* A helper class for specifying conditions in a V4 POST Policy document. Used in: {@link
* Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4,
* PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)}.
*
* @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document">
* Policy document</a>
Expand All @@ -183,14 +260,14 @@ public static Builder newBuilder() {
}

public Set<ConditionV4> getConditions() {
return conditions;
return Collections.unmodifiableSet(conditions);
}

public static class Builder {
Set<ConditionV4> conditions;
private final Set<ConditionV4> conditions;

private Builder() {
this.conditions = new LinkedHashSet<>();
this(new LinkedHashSet<ConditionV4>());
}

private Builder(Set<ConditionV4> conditions) {
Expand All @@ -206,64 +283,93 @@ public PostConditionsV4 build() {
}

public Builder addAclCondition(ConditionV4Type type, String acl) {
checkType(type, "acl");
conditions.add(new ConditionV4(type, "acl", acl));
return this;
}

public Builder addBucketCondition(ConditionV4Type type, String bucket) {
checkType(type, "bucket");
conditions.add(new ConditionV4(type, "bucket", bucket));
return this;
}

public Builder addCacheControlCondition(ConditionV4Type type, String cacheControl) {
checkType(type, "cache-control");
conditions.add(new ConditionV4(type, "cache-control", cacheControl));
return this;
}

public Builder addContentDispositionCondition(
ConditionV4Type type, String contentDisposition) {
checkType(type, "content-disposition");
conditions.add(new ConditionV4(type, "content-disposition", contentDisposition));
return this;
}

public Builder addContentEncodingCondition(ConditionV4Type type, String contentEncoding) {
checkType(type, "content-encoding");
conditions.add(new ConditionV4(type, "content-encoding", contentEncoding));
return this;
}

/**
* @deprecated Invocation of this method has no effect. Use {@link
* #addContentLengthRangeCondition(int, int)} to specify a range for the content-length.
*/
public Builder addContentLengthCondition(ConditionV4Type type, int contentLength) {
conditions.add(new ConditionV4(type, "content-length", "" + contentLength));
return this;
}

public Builder addContentTypeCondition(ConditionV4Type type, String contentType) {
checkType(type, "content-type");
conditions.add(new ConditionV4(type, "content-type", contentType));
return this;
}

/** @deprecated Use {@link #addExpiresCondition(long)} */
@Deprecated
public Builder addExpiresCondition(ConditionV4Type type, long expires) {
conditions.add(new ConditionV4(type, "expires", dateFormat.format(expires)));
return this;
return addExpiresCondition(expires);
}

/** @deprecated Use {@link #addExpiresCondition(String)} */
@Deprecated
public Builder addExpiresCondition(ConditionV4Type type, String expires) {
conditions.add(new ConditionV4(type, "expires", expires));
return addExpiresCondition(expires);
}

public Builder addExpiresCondition(long expires) {
return addExpiresCondition(dateFormat.format(expires));
}

public Builder addExpiresCondition(String expires) {
conditions.add(new ConditionV4(ConditionV4Type.MATCHES, "expires", expires));
return this;
}

public Builder addKeyCondition(ConditionV4Type type, String key) {
checkType(type, "key");
conditions.add(new ConditionV4(type, "key", key));
return this;
}

public Builder addSuccessActionRedirectUrlCondition(
ConditionV4Type type, String successActionRedirectUrl) {
checkType(type, "success_action_redirect");
conditions.add(new ConditionV4(type, "success_action_redirect", successActionRedirectUrl));
return this;
}

/** @deprecated Use {@link #addSuccessActionStatusCondition(int)} */
@Deprecated
public Builder addSuccessActionStatusCondition(ConditionV4Type type, int status) {
conditions.add(new ConditionV4(type, "success_action_status", "" + status));
return addSuccessActionStatusCondition(status);
}

public Builder addSuccessActionStatusCondition(int status) {
conditions.add(
new ConditionV4(ConditionV4Type.MATCHES, "success_action_status", "" + status));
return this;
}

Expand All @@ -276,11 +382,17 @@ Builder addCustomCondition(ConditionV4Type type, String field, String value) {
conditions.add(new ConditionV4(type, field, value));
return this;
}

private void checkType(ConditionV4Type type, String field) {
if (type != ConditionV4Type.MATCHES && type != ConditionV4Type.STARTS_WITH) {
throw new IllegalArgumentException("Field " + field + " can't use " + type);
}
}
}
}

/**
* Class for a V4 POST Policy document.
* Class for a V4 POST Policy document. Used by Storage to construct {@code PostPolicyV4} objects.
*
* @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document">
* Policy document</a>
Expand Down Expand Up @@ -367,9 +479,20 @@ public String toJson() {
}

public enum ConditionV4Type {
MATCHES,
STARTS_WITH,
CONTENT_LENGTH_RANGE
MATCHES("eq"),
STARTS_WITH("starts-with"),
CONTENT_LENGTH_RANGE("content-length-range");

private final String name;

ConditionV4Type(String name) {
this.name = name;
}

@Override
public String toString() {
return name;
}
}

/**
Expand All @@ -378,12 +501,12 @@ public enum ConditionV4Type {
* @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document">
* Policy document</a>
*/
static final class ConditionV4 {
final ConditionV4Type type;
final String operand1;
final String operand2;
public static final class ConditionV4 {
public final ConditionV4Type type;
public final String operand1;
public final String operand2;

private ConditionV4(ConditionV4Type type, String operand1, String operand2) {
ConditionV4(ConditionV4Type type, String operand1, String operand2) {
this.type = type;
this.operand1 = operand1;
this.operand2 = operand2;
Expand All @@ -401,5 +524,18 @@ public boolean equals(Object other) {
public int hashCode() {
return Objects.hash(type, operand1, operand2);
}

/**
* Examples of returned strings: {@code ["eq", "$key", "test-object"]}, {@code ["starts-with",
* "$acl", "public"]}, {@code ["content-length-range", 246, 266]}.
*/
@Override
public String toString() {
String body =
type == ConditionV4Type.CONTENT_LENGTH_RANGE
? operand1 + ", " + operand2
: "\"$" + operand1 + "\", \"" + operand2 + "\"";
return "[\"" + type + "\", " + body + "]";
}
}
}

0 comments on commit 8602b81

Please sign in to comment.