Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: PostPolicyV4 classes could be improved #442

Merged
merged 9 commits into from Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
dmitry-fa marked this conversation as resolved.
Show resolved Hide resolved
}
PostFieldsV4.validateFields(fields);

this.url = url;
this.fields = fields;
this.fields = Collections.unmodifiableMap(fields);
dmitry-fa marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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);
dmitry-fa marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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 + "]";
}
}
}