Repository: getmoto/moto

Issue:
S3 custom metadata key should allow dotted values
## Reporting Bugs

S3 custom metadata should allow the use of the "dot" as part of the key name.

The regex in https://github.com/spulec/moto/blob/master/moto/s3/utils.py#L76 prevents users from doing so hence rejecting any metadata of the following form `foo.bar = something`

### Pytest test case to reproduce the issue:

```
import boto3
from moto import mock_s3


class TestS3Metadata:
    def test_s3_metadata(self):
        bucket_name = "test-medata"
        metadata = {
            "foo": "FOO",
            "bar": "BAR",
            "foo.bar": "FOO.BAR"
        }
        with mock_s3():
            s3 = boto3.client("s3")

            # Create an S3 bucket and store a file with metadata
            s3.create_bucket(Bucket=bucket_name)
            s3.put_object(
                Bucket=bucket_name,
                Key="test.txt",
                Body="File with meta",
                Metadata=metadata,
            )
            # Retrieve previously stored object metadata
            obj = s3.head_object(
                Bucket=bucket_name,
                Key="test.txt",
            )

            assert obj['Metadata'] == metadata
```

* Output of the test:

```
>           assert obj['Metadata'] == metadata
E           AssertionError: assert {'bar': 'BAR', 'foo': 'FOO'} == {'bar': 'BAR', 'foo': 'FOO', 'foo.bar': 'FOO.BAR'}
E             Common items:
E             {'bar': 'BAR', 'foo': 'FOO'}
E             Right contains 1 more item:
E             {'foo.bar': 'FOO.BAR'}
E             Full diff:
E             - {'bar': 'BAR', 'foo': 'FOO', 'foo.bar': 'FOO.BAR'}
E             + {'bar': 'BAR', 'foo': 'FOO'}

test.py:30: AssertionError
=================================================================== short test summary info ===================================================================
FAILED test.py::TestS3Metadata::test_s3_metadata - AssertionError: assert {'bar': 'BAR', 'foo': 'FOO'} == {'bar': 'BAR', 'foo': 'FOO', 'foo.bar': 'FOO.BAR'}
====================================================================== 1 failed in 1.01s ======================================================================
```

### Expected result

Test should pass as `foo.bar` is a valid S3 metadata

### What actually happens

Pip installs:
* `moto==2.0.8`
* `boto3==1.17.79`
* `botocore==1.20.79`


Discussion:

Resolving patch:
diff --git a/moto/s3/utils.py b/moto/s3/utils.py
--- a/moto/s3/utils.py
+++ b/moto/s3/utils.py
@@ -73,7 +73,7 @@ def parse_region_from_url(url):
 
 def metadata_from_headers(headers):
     metadata = CaseInsensitiveDict()
-    meta_regex = re.compile(r"^x-amz-meta-([a-zA-Z0-9\-_]+)$", flags=re.IGNORECASE)
+    meta_regex = re.compile(r"^x-amz-meta-([a-zA-Z0-9\-_.]+)$", flags=re.IGNORECASE)
     for header, value in headers.items():
         if isinstance(header, six.string_types):
             result = meta_regex.match(header)


Test patch:
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -65,13 +65,16 @@ def wrapped(*args, **kwargs):
 
 
 class MyModel(object):
-    def __init__(self, name, value):
+    def __init__(self, name, value, metadata={}):
         self.name = name
         self.value = value
+        self.metadata = metadata
 
     def save(self):
         s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-        s3.put_object(Bucket="mybucket", Key=self.name, Body=self.value)
+        s3.put_object(
+            Bucket="mybucket", Key=self.name, Body=self.value, Metadata=self.metadata
+        )
 
 
 @mock_s3
@@ -133,6 +136,24 @@ def test_my_model_save():
     assert body == "is awesome"
 
 
+@mock_s3
+def test_object_metadata():
+    """Metadata keys can contain certain special characters like dash and dot"""
+    # Create Bucket so that test can run
+    conn = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    conn.create_bucket(Bucket="mybucket")
+    ####################################
+
+    metadata = {"meta": "simple", "my-meta": "dash", "meta.data": "namespaced"}
+
+    model_instance = MyModel("steve", "is awesome", metadata=metadata)
+    model_instance.save()
+
+    meta = conn.Object("mybucket", "steve").get()["Metadata"]
+
+    assert meta == metadata
+
+
 @mock_s3
 def test_key_etag():
     conn = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)