diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 22cd45c08..6ac139a14 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -5,9 +5,10 @@ import sys import six from botocore.awsrequest import AWSPreparedRequest +from werkzeug.wrappers import Request from moto.core.utils import str_to_rfc_1123_datetime, py2_strip_unicode_keys -from six.moves.urllib.parse import parse_qs, urlparse, unquote +from six.moves.urllib.parse import parse_qs, urlparse, unquote, parse_qsl import xmltodict @@ -777,6 +778,7 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): return 409, {}, template.render(bucket=removed_bucket) def _bucket_response_post(self, request, body, bucket_name): + response_headers = {} if not request.headers.get("Content-Length"): return 411, {}, "Content-Length required" @@ -795,14 +797,18 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): if hasattr(request, "form"): # Not HTTPretty form = request.form + elif request.headers.get("Content-Type").startswith("multipart/form-data"): + request = Request.from_values( + input_stream=six.BytesIO(request.body), + content_length=request.headers["Content-Length"], + content_type=request.headers["Content-Type"], + method="POST", + ) + form = request.form else: # HTTPretty, build new form object body = body.decode() - - form = {} - for kv in body.split("&"): - k, v = kv.split("=") - form[k] = v + form = dict(parse_qsl(body)) key = form["key"] if "file" in form: @@ -810,13 +816,23 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): else: f = request.files["file"].stream.read() + if "success_action_redirect" in form: + response_headers["Location"] = form["success_action_redirect"] + + if "success_action_status" in form: + status_code = form["success_action_status"] + elif "success_action_redirect" in form: + status_code = 303 + else: + status_code = 204 + new_key = self.backend.set_key(bucket_name, key, f) # Metadata metadata = metadata_from_headers(form) new_key.set_metadata(metadata) - return 200, {}, "" + return status_code, response_headers, "" @staticmethod def _get_path(request): diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 303ed523d..ffbd73966 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -14,6 +14,7 @@ from io import BytesIO import mimetypes import zlib import pickle +import uuid import json import boto @@ -4428,3 +4429,41 @@ def test_s3_config_dict(): assert not logging_bucket["supplementaryConfiguration"].get( "BucketTaggingConfiguration" ) + + +@mock_s3 +def test_creating_presigned_post(): + bucket = "presigned-test" + s3 = boto3.client("s3", region_name="us-east-1") + s3.create_bucket(Bucket=bucket) + success_url = "http://localhost/completed" + fdata = b"test data\n" + file_uid = uuid.uuid4() + conditions = [ + {"Content-Type": "text/plain"}, + {"x-amz-server-side-encryption": "AES256"}, + {"success_action_redirect": success_url}, + ] + conditions.append(["content-length-range", 1, 30]) + data = s3.generate_presigned_post( + Bucket=bucket, + Key="{file_uid}.txt".format(file_uid=file_uid), + Fields={ + "content-type": "text/plain", + "success_action_redirect": success_url, + "x-amz-server-side-encryption": "AES256", + }, + Conditions=conditions, + ExpiresIn=1000, + ) + resp = requests.post( + data["url"], data=data["fields"], files={"file": fdata}, allow_redirects=False + ) + assert resp.headers["Location"] == success_url + assert resp.status_code == 303 + assert ( + s3.get_object(Bucket=bucket, Key="{file_uid}.txt".format(file_uid=file_uid))[ + "Body" + ].read() + == fdata + )