1/*
2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage
3 * Copyright 2015-2017 MinIO, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package minio
19
20import (
21	"encoding/base64"
22	"fmt"
23	"strings"
24	"time"
25)
26
27// expirationDateFormat date format for expiration key in json policy.
28const expirationDateFormat = "2006-01-02T15:04:05.999Z"
29
30// policyCondition explanation:
31// http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
32//
33// Example:
34//
35//   policyCondition {
36//       matchType: "$eq",
37//       key: "$Content-Type",
38//       value: "image/png",
39//   }
40//
41type policyCondition struct {
42	matchType string
43	condition string
44	value     string
45}
46
47// PostPolicy - Provides strict static type conversion and validation
48// for Amazon S3's POST policy JSON string.
49type PostPolicy struct {
50	// Expiration date and time of the POST policy.
51	expiration time.Time
52	// Collection of different policy conditions.
53	conditions []policyCondition
54	// ContentLengthRange minimum and maximum allowable size for the
55	// uploaded content.
56	contentLengthRange struct {
57		min int64
58		max int64
59	}
60
61	// Post form data.
62	formData map[string]string
63}
64
65// NewPostPolicy - Instantiate new post policy.
66func NewPostPolicy() *PostPolicy {
67	p := &PostPolicy{}
68	p.conditions = make([]policyCondition, 0)
69	p.formData = make(map[string]string)
70	return p
71}
72
73// SetExpires - Sets expiration time for the new policy.
74func (p *PostPolicy) SetExpires(t time.Time) error {
75	if t.IsZero() {
76		return errInvalidArgument("No expiry time set.")
77	}
78	p.expiration = t
79	return nil
80}
81
82// SetKey - Sets an object name for the policy based upload.
83func (p *PostPolicy) SetKey(key string) error {
84	if strings.TrimSpace(key) == "" || key == "" {
85		return errInvalidArgument("Object name is empty.")
86	}
87	policyCond := policyCondition{
88		matchType: "eq",
89		condition: "$key",
90		value:     key,
91	}
92	if err := p.addNewPolicy(policyCond); err != nil {
93		return err
94	}
95	p.formData["key"] = key
96	return nil
97}
98
99// SetKeyStartsWith - Sets an object name that an policy based upload
100// can start with.
101func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error {
102	if strings.TrimSpace(keyStartsWith) == "" || keyStartsWith == "" {
103		return errInvalidArgument("Object prefix is empty.")
104	}
105	policyCond := policyCondition{
106		matchType: "starts-with",
107		condition: "$key",
108		value:     keyStartsWith,
109	}
110	if err := p.addNewPolicy(policyCond); err != nil {
111		return err
112	}
113	p.formData["key"] = keyStartsWith
114	return nil
115}
116
117// SetBucket - Sets bucket at which objects will be uploaded to.
118func (p *PostPolicy) SetBucket(bucketName string) error {
119	if strings.TrimSpace(bucketName) == "" || bucketName == "" {
120		return errInvalidArgument("Bucket name is empty.")
121	}
122	policyCond := policyCondition{
123		matchType: "eq",
124		condition: "$bucket",
125		value:     bucketName,
126	}
127	if err := p.addNewPolicy(policyCond); err != nil {
128		return err
129	}
130	p.formData["bucket"] = bucketName
131	return nil
132}
133
134// SetCondition - Sets condition for credentials, date and algorithm
135func (p *PostPolicy) SetCondition(matchType, condition, value string) error {
136	if strings.TrimSpace(value) == "" || value == "" {
137		return errInvalidArgument("No value specified for condition")
138	}
139
140	policyCond := policyCondition{
141		matchType: matchType,
142		condition: "$" + condition,
143		value:     value,
144	}
145	if condition == "X-Amz-Credential" || condition == "X-Amz-Date" || condition == "X-Amz-Algorithm" {
146		if err := p.addNewPolicy(policyCond); err != nil {
147			return err
148		}
149		p.formData[condition] = value
150		return nil
151	}
152	return errInvalidArgument("Invalid condition in policy")
153}
154
155// SetContentType - Sets content-type of the object for this policy
156// based upload.
157func (p *PostPolicy) SetContentType(contentType string) error {
158	if strings.TrimSpace(contentType) == "" || contentType == "" {
159		return errInvalidArgument("No content type specified.")
160	}
161	policyCond := policyCondition{
162		matchType: "eq",
163		condition: "$Content-Type",
164		value:     contentType,
165	}
166	if err := p.addNewPolicy(policyCond); err != nil {
167		return err
168	}
169	p.formData["Content-Type"] = contentType
170	return nil
171}
172
173// SetContentTypeStartsWith - Sets what content-type of the object for this policy
174// based upload can start with.
175func (p *PostPolicy) SetContentTypeStartsWith(contentTypeStartsWith string) error {
176	if strings.TrimSpace(contentTypeStartsWith) == "" || contentTypeStartsWith == "" {
177		return errInvalidArgument("No content type specified.")
178	}
179	policyCond := policyCondition{
180		matchType: "starts-with",
181		condition: "$Content-Type",
182		value:     contentTypeStartsWith,
183	}
184	if err := p.addNewPolicy(policyCond); err != nil {
185		return err
186	}
187	p.formData["Content-Type"] = contentTypeStartsWith
188	return nil
189}
190
191// SetContentLengthRange - Set new min and max content length
192// condition for all incoming uploads.
193func (p *PostPolicy) SetContentLengthRange(min, max int64) error {
194	if min > max {
195		return errInvalidArgument("Minimum limit is larger than maximum limit.")
196	}
197	if min < 0 {
198		return errInvalidArgument("Minimum limit cannot be negative.")
199	}
200	if max < 0 {
201		return errInvalidArgument("Maximum limit cannot be negative.")
202	}
203	p.contentLengthRange.min = min
204	p.contentLengthRange.max = max
205	return nil
206}
207
208// SetSuccessActionRedirect - Sets the redirect success url of the object for this policy
209// based upload.
210func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error {
211	if strings.TrimSpace(redirect) == "" || redirect == "" {
212		return errInvalidArgument("Redirect is empty")
213	}
214	policyCond := policyCondition{
215		matchType: "eq",
216		condition: "$success_action_redirect",
217		value:     redirect,
218	}
219	if err := p.addNewPolicy(policyCond); err != nil {
220		return err
221	}
222	p.formData["success_action_redirect"] = redirect
223	return nil
224}
225
226// SetSuccessStatusAction - Sets the status success code of the object for this policy
227// based upload.
228func (p *PostPolicy) SetSuccessStatusAction(status string) error {
229	if strings.TrimSpace(status) == "" || status == "" {
230		return errInvalidArgument("Status is empty")
231	}
232	policyCond := policyCondition{
233		matchType: "eq",
234		condition: "$success_action_status",
235		value:     status,
236	}
237	if err := p.addNewPolicy(policyCond); err != nil {
238		return err
239	}
240	p.formData["success_action_status"] = status
241	return nil
242}
243
244// SetUserMetadata - Set user metadata as a key/value couple.
245// Can be retrieved through a HEAD request or an event.
246func (p *PostPolicy) SetUserMetadata(key string, value string) error {
247	if strings.TrimSpace(key) == "" || key == "" {
248		return errInvalidArgument("Key is empty")
249	}
250	if strings.TrimSpace(value) == "" || value == "" {
251		return errInvalidArgument("Value is empty")
252	}
253	headerName := fmt.Sprintf("x-amz-meta-%s", key)
254	policyCond := policyCondition{
255		matchType: "eq",
256		condition: fmt.Sprintf("$%s", headerName),
257		value:     value,
258	}
259	if err := p.addNewPolicy(policyCond); err != nil {
260		return err
261	}
262	p.formData[headerName] = value
263	return nil
264}
265
266// SetUserData - Set user data as a key/value couple.
267// Can be retrieved through a HEAD request or an event.
268func (p *PostPolicy) SetUserData(key string, value string) error {
269	if key == "" {
270		return errInvalidArgument("Key is empty")
271	}
272	if value == "" {
273		return errInvalidArgument("Value is empty")
274	}
275	headerName := fmt.Sprintf("x-amz-%s", key)
276	policyCond := policyCondition{
277		matchType: "eq",
278		condition: fmt.Sprintf("$%s", headerName),
279		value:     value,
280	}
281	if err := p.addNewPolicy(policyCond); err != nil {
282		return err
283	}
284	p.formData[headerName] = value
285	return nil
286}
287
288// addNewPolicy - internal helper to validate adding new policies.
289func (p *PostPolicy) addNewPolicy(policyCond policyCondition) error {
290	if policyCond.matchType == "" || policyCond.condition == "" || policyCond.value == "" {
291		return errInvalidArgument("Policy fields are empty.")
292	}
293	p.conditions = append(p.conditions, policyCond)
294	return nil
295}
296
297// String function for printing policy in json formatted string.
298func (p PostPolicy) String() string {
299	return string(p.marshalJSON())
300}
301
302// marshalJSON - Provides Marshaled JSON in bytes.
303func (p PostPolicy) marshalJSON() []byte {
304	expirationStr := `"expiration":"` + p.expiration.Format(expirationDateFormat) + `"`
305	var conditionsStr string
306	conditions := []string{}
307	for _, po := range p.conditions {
308		conditions = append(conditions, fmt.Sprintf("[\"%s\",\"%s\",\"%s\"]", po.matchType, po.condition, po.value))
309	}
310	if p.contentLengthRange.min != 0 || p.contentLengthRange.max != 0 {
311		conditions = append(conditions, fmt.Sprintf("[\"content-length-range\", %d, %d]",
312			p.contentLengthRange.min, p.contentLengthRange.max))
313	}
314	if len(conditions) > 0 {
315		conditionsStr = `"conditions":[` + strings.Join(conditions, ",") + "]"
316	}
317	retStr := "{"
318	retStr = retStr + expirationStr + ","
319	retStr = retStr + conditionsStr
320	retStr = retStr + "}"
321	return []byte(retStr)
322}
323
324// base64 - Produces base64 of PostPolicy's Marshaled json.
325func (p PostPolicy) base64() string {
326	return base64.StdEncoding.EncodeToString(p.marshalJSON())
327}
328