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
feat: V4 POST policy #177
feat: V4 POST policy #177
Changes from 21 commits
a76845a
9d6261a
73a2da5
25ce31d
aca896e
161fcb5
a485ac0
9cf145a
362ce2c
ee7c507
140c26c
821dddb
440a1ca
0105b9d
2ed70e6
8db9aa2
18a3108
165811d
391772c
fe965fc
00c9dc3
3b2834a
9c445f4
abbb7d8
d83d431
52519a7
5b53ebe
15a0f4c
cea97ff
721edba
bbbee21
a5e48d9
14c66f3
4b49abf
7be0222
2274bda
a413ae6
c323ac9
1d959fb
da40d03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,374 @@ | ||
package com.google.cloud.storage; | ||
|
||
import com.google.gson.JsonArray; | ||
import com.google.gson.JsonObject; | ||
|
||
|
||
import java.text.SimpleDateFormat; | ||
import java.util.HashMap; | ||
import java.util.LinkedHashSet; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
|
||
/** | ||
* Presigned V4 post policy. | ||
* | ||
* @see <a href="https://cloud.google.com/storage/docs/xml-api/post-object" POST Object</a> | ||
*/ | ||
public final class PostPolicyV4 { | ||
private String url; | ||
private Map<String, String> fields; | ||
|
||
private PostPolicyV4(String url, Map<String, String> fields) { | ||
this.url = url; | ||
this.fields = fields; | ||
} | ||
|
||
public static PostPolicyV4 of(String url, Map<String, String> fields) { | ||
return new PostPolicyV4(url, fields); | ||
} | ||
|
||
public String getUrl() { | ||
return url; | ||
} | ||
|
||
public Map<String, String> getFields() { | ||
return fields; | ||
} | ||
|
||
/** | ||
* Class representing which fields to specify in a V4 POST request. | ||
* | ||
* @see <a href=https://cloud.google.com/storage/docs/xml-api/post-object#form_fields> POST Object Form fields</a> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No double quote |
||
*/ | ||
public static final class PostFieldsV4 { | ||
private Map<String, String> fieldsMap; | ||
|
||
private PostFieldsV4(Builder builder) { | ||
this.fieldsMap = new HashMap<>(); | ||
fieldsMap.put("acl", builder.acl); | ||
fieldsMap.put("cache-control", builder.cacheControl); | ||
fieldsMap.put("content-disposition", builder.contentDisposition); | ||
fieldsMap.put("content-length", builder.contentLength); | ||
fieldsMap.put("content-encoding", builder.contentEncoding); | ||
fieldsMap.put("content-type", builder.contentType); | ||
fieldsMap.put("expires", builder.expires); | ||
fieldsMap.put("success_action_redirect", builder.successActionRedirect); | ||
fieldsMap.put("success_action_status", builder.successActionStatus); | ||
} | ||
|
||
private PostFieldsV4(Map<String, String> fields) { | ||
this.fieldsMap = fields; | ||
} | ||
|
||
public static PostFieldsV4 of(Map<String, String> fields) { | ||
return new PostFieldsV4(fields); | ||
} | ||
|
||
public static Builder newBuilder() { | ||
return new Builder(); | ||
} | ||
|
||
public Map<String, String> getFieldsMap() { | ||
return fieldsMap; | ||
} | ||
|
||
public static class Builder { | ||
private String acl; | ||
private String cacheControl; | ||
private String contentDisposition; | ||
private String contentEncoding; | ||
private String contentLength; | ||
private String contentType; | ||
private String expires; | ||
private String successActionRedirect; | ||
private String successActionStatus; | ||
|
||
private Builder() { | ||
} | ||
|
||
public PostFieldsV4 build() { | ||
return new PostFieldsV4(this); | ||
} | ||
|
||
public Builder setAcl(String acl) { | ||
this.acl = acl; | ||
return this; | ||
} | ||
|
||
public Builder setCacheControl(String cacheControl) { | ||
this.cacheControl = cacheControl; | ||
return this; | ||
} | ||
|
||
public Builder setContentDisposition(String contentDisposition) { | ||
this.contentDisposition = contentDisposition; | ||
return this; | ||
} | ||
|
||
public Builder setContentEncoding(String contentEncoding) { | ||
this.contentEncoding = contentEncoding; | ||
return this; | ||
} | ||
|
||
public Builder setContentLength(int contentLength) { | ||
this.contentLength = "" + contentLength; | ||
return this; | ||
} | ||
|
||
public Builder setContentType(String contentType) { | ||
this.contentType = contentType; | ||
return this; | ||
} | ||
|
||
public Builder Expires(String expires) { | ||
this.expires = expires; | ||
return this; | ||
} | ||
|
||
public Builder setSuccessActionRedirect(String successActionRedirect) { | ||
this.successActionRedirect = successActionRedirect; | ||
return this; | ||
} | ||
|
||
public Builder setSuccessActionStatus(int successActionStatus) { | ||
this.successActionStatus = "" + successActionStatus; | ||
return this; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Custom field setter should be available here. |
||
} | ||
} | ||
|
||
/** | ||
* Class for specifying conditions in a V4 POST Policy document. | ||
* | ||
* @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document"> Policy document</a> | ||
*/ | ||
public static final class PostConditionsV4 { | ||
private Set<ConditionV4> conditions; | ||
|
||
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); | ||
|
||
public PostConditionsV4(Builder builder) { | ||
this.conditions = builder.conditions; | ||
} | ||
|
||
public Builder toBuilder() { | ||
return new Builder(conditions); | ||
} | ||
|
||
public static Builder newBuilder() { | ||
return new Builder(); | ||
} | ||
|
||
public Set<ConditionV4> getConditions() { | ||
return conditions; | ||
} | ||
|
||
public static class Builder { | ||
Set<ConditionV4> conditions; | ||
|
||
private Builder() { | ||
this.conditions = new LinkedHashSet<>(); | ||
} | ||
|
||
private Builder(Set<ConditionV4> conditions) { | ||
this.conditions = conditions; | ||
} | ||
|
||
public static Builder newBuilder() { | ||
return new Builder(); | ||
} | ||
|
||
public PostConditionsV4 build() { | ||
return new PostConditionsV4(this); | ||
} | ||
|
||
public Builder addAclCondition(ConditionV4Type type, String acl) { | ||
conditions.add(new ConditionV4(type, "acl", acl)); | ||
return this; | ||
} | ||
|
||
public Builder addBucketCondition(ConditionV4Type type, String bucket) { | ||
conditions.add(new ConditionV4(type, "bucket", bucket)); | ||
return this; | ||
} | ||
|
||
public Builder addCacheControlCondition(ConditionV4Type type, String cacheControl) { | ||
conditions.add(new ConditionV4(type, "cache-control", cacheControl)); | ||
return this; | ||
} | ||
|
||
public Builder addContentDispositionCondition( | ||
ConditionV4Type type, String contentDisposition) { | ||
conditions.add(new ConditionV4(type, "content-disposition", contentDisposition)); | ||
return this; | ||
} | ||
|
||
public Builder addContentEncodingCondition(ConditionV4Type type, String contentEncoding) { | ||
conditions.add(new ConditionV4(type, "content-encoding", contentEncoding)); | ||
return this; | ||
} | ||
|
||
public Builder addContentLengthCondition(ConditionV4Type type, int contentLength) { | ||
conditions.add(new ConditionV4(type, "content-length", "" + contentLength)); | ||
return this; | ||
} | ||
|
||
public Builder addContentTypeCondition(ConditionV4Type type, String contentType) { | ||
conditions.add(new ConditionV4(type, "content-type", contentType)); | ||
return this; | ||
} | ||
|
||
public Builder addExpiresCondition(ConditionV4Type type, long expires) { | ||
conditions.add(new ConditionV4(type, "expires", dateFormat.format(expires))); | ||
return this; | ||
} | ||
|
||
public Builder addExpiresCondition(ConditionV4Type type, String expires) { | ||
conditions.add(new ConditionV4(type, "expires", expires)); | ||
return this; | ||
} | ||
|
||
public Builder addKeyCondition(ConditionV4Type type, String key) { | ||
conditions.add(new ConditionV4(type, "key", key)); | ||
return this; | ||
} | ||
|
||
public Builder addSuccessActionRedirectUrlCondition( | ||
ConditionV4Type type, String successActionRedirectUrl) { | ||
conditions.add( | ||
new ConditionV4(type, "success_action_redirect", successActionRedirectUrl)); | ||
return this; | ||
} | ||
|
||
public Builder addSuccessActionStatusCondition(ConditionV4Type type, int status) { | ||
conditions.add(new ConditionV4(type, "success_action_status", "" + status)); | ||
return this; | ||
} | ||
|
||
public Builder addCustomMetadataCondition(ConditionV4Type type, String field, String value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be in fields instead. |
||
conditions.add(new ConditionV4(type, "x-goog-meta-" + field, value)); | ||
return this; | ||
} | ||
|
||
public Builder addContentLengthRangeCondition(int min, int max) { | ||
conditions.add( | ||
new ConditionV4(ConditionV4Type.CONTENT_LENGTH_RANGE, "" + min, "" + max)); | ||
return this; | ||
} | ||
|
||
public Builder addCustomCondition(ConditionV4Type type, String field, String value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest removing this until we get a feature request for this method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually use it in the generate method, but I can change it to package private There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. package private sgtm, thanks for clarifying! |
||
conditions.add(new ConditionV4(type, field, value)); | ||
return this; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Class for a V4 POST Policy document. | ||
* | ||
* @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document"> Policy document</a> | ||
*/ | ||
public static final class PostPolicyV4Document { | ||
private String expiration; | ||
private PostConditionsV4 conditions; | ||
|
||
private PostPolicyV4Document(String expiration, PostConditionsV4 conditions) { | ||
this.expiration = expiration; | ||
this.conditions = conditions; | ||
} | ||
|
||
public static PostPolicyV4Document of(String expiration, PostConditionsV4 conditions) { | ||
return new PostPolicyV4Document(expiration, conditions); | ||
} | ||
|
||
public String toJson() { | ||
JsonObject object = new JsonObject(); | ||
JsonArray conditions = new JsonArray(); | ||
for (ConditionV4 condition : this.conditions.conditions) { | ||
switch (condition.type) { | ||
case MATCHES: | ||
JsonObject match = new JsonObject(); | ||
match.addProperty(condition.operand1, condition.operand2); | ||
conditions.add(match); | ||
break; | ||
case STARTS_WITH: | ||
JsonArray startsWith = new JsonArray(); | ||
startsWith.add("starts-with"); | ||
startsWith.add("$" + condition.operand1); | ||
startsWith.add(condition.operand2); | ||
conditions.add(startsWith); | ||
break; | ||
case CONTENT_LENGTH_RANGE: | ||
JsonArray contentLengthRange = new JsonArray(); | ||
contentLengthRange.add("content-length-range"); | ||
contentLengthRange.add(Integer.parseInt(condition.operand1)); | ||
contentLengthRange.add(Integer.parseInt(condition.operand2)); | ||
conditions.add(contentLengthRange); | ||
break; | ||
} | ||
} | ||
object.add("conditions", conditions); | ||
object.addProperty("expiration", expiration); | ||
|
||
String json = object.toString(); | ||
StringBuilder escapedJson = new StringBuilder(); | ||
|
||
//Certain characters in a policy must be escaped | ||
for (char c : json.toCharArray()) { | ||
if (c >= 128) { //is a unicode character | ||
escapedJson.append(String.format("\\u%04x", (int) c)); | ||
} else { | ||
switch (c) { | ||
case '\\': escapedJson.append("\\\\"); break; | ||
case '\b' : escapedJson.append("\\b"); break; | ||
case '\f' : escapedJson.append("\\f"); break; | ||
case '\n' : escapedJson.append("\\n"); break; | ||
case '\r' : escapedJson.append("\\r"); break; | ||
case '\t' : escapedJson.append("\\t"); break; | ||
case '\u000b' : escapedJson.append("\\v"); break; | ||
default : escapedJson.append(c); | ||
} | ||
} | ||
} | ||
return escapedJson.toString(); | ||
} | ||
} | ||
|
||
enum ConditionV4Type { | ||
MATCHES, | ||
STARTS_WITH, | ||
CONTENT_LENGTH_RANGE | ||
} | ||
|
||
/** | ||
* Class for a specific POST policy document condition. | ||
* | ||
* @see <a href="https://cloud.google.com/storage/docs/authentication/signatures#policy-document"> Policy document</a> | ||
*/ | ||
static final class ConditionV4 { | ||
ConditionV4Type type; | ||
String operand1; | ||
String operand2; | ||
|
||
private ConditionV4(ConditionV4Type type, String operand1, String operand2) { | ||
this.type = type; | ||
this.operand1 = operand1; | ||
this.operand2 = operand2; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object other) { | ||
ConditionV4 condition = (ConditionV4) other; | ||
return this.type == condition.type | ||
&& this.operand1.equals(condition.operand1) | ||
&& this.operand2.equals(condition.operand2); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(type, operand1, operand2); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
>