diff --git a/s3tests/functional/test_s3_website.py b/s3tests/functional/test_s3_website.py
new file mode 100644
index 0000000..74e0c81
--- /dev/null
+++ b/s3tests/functional/test_s3_website.py
@@ -0,0 +1,525 @@
+from __future__ import print_function
+import sys
+from cStringIO import StringIO
+import boto.exception
+import boto.s3.connection
+import boto.s3.acl
+import bunch
+import datetime
+import time
+import email.utils
+import isodate
+import nose
+import operator
+import socket
+import ssl
+import os
+import requests
+import base64
+import hmac
+import sha
+import pytz
+import json
+import httplib2
+import threading
+import itertools
+import string
+import random
+
+import xml.etree.ElementTree as ET
+
+from httplib import HTTPConnection, HTTPSConnection
+from urlparse import urlparse
+
+from nose.tools import eq_ as eq, ok_ as ok
+from nose.plugins.attrib import attr
+from nose.plugins.skip import SkipTest
+
+from .utils import assert_raises
+from .utils import generate_random
+from .utils import region_sync_meta
+import AnonymousAuth
+
+from email.header import decode_header
+from ordereddict import OrderedDict
+
+from boto.s3.cors import CORSConfiguration
+
+from . import (
+ get_new_bucket,
+ get_new_bucket_name,
+ s3,
+ config,
+ _make_raw_request,
+ choose_bucket_prefix,
+ )
+
+WEBSITE_CONFIGS_XMLFRAG = {
+ 'IndexDoc': '{indexdoc}',
+ 'IndexDocErrorDoc': '{indexdoc}{errordoc}',
+ }
+
+def make_website_config(xml_fragment):
+ """
+ Take the tedious stuff out of the config
+ """
+ return '' + xml_fragment + ''
+
+def get_website_url(proto, bucket, path):
+ """
+ Return the URL to a website page
+ """
+ domain = config['main']['host']
+ if('s3website_domain' in config['main']):
+ domain = config['main']['s3website_domain']
+ elif('s3website_domain' in config['alt']):
+ domain = config['DEFAULT']['s3website_domain']
+ return "%s://%s.%s/%s" % (proto, bucket, domain, path)
+
+def _test_website_prep(bucket, xml_fragment):
+ indexname = choose_bucket_prefix(template='index-{random}.html', max_len=32)
+ errorname = choose_bucket_prefix(template='error-{random}.html', max_len=32)
+ xml_fragment = xml_fragment.format(indexdoc=indexname, errordoc=errorname)
+ config_xml = make_website_config(xml_fragment)
+ bucket.set_website_configuration_xml(config_xml)
+ eq (config_xml, bucket.get_website_configuration_xml())
+ return indexname, errorname
+
+def __website_expected_reponse_status(res, status, reason):
+ eq(res.status, status)
+ eq(res.reason, reason)
+
+def _website_expected_error_response(res, bucket_name, status, reason, code):
+ body = res.read()
+ print(body)
+ __website_expected_reponse_status(res, status, reason)
+ ok('
Code: '+code+'' in body, 'HTML should contain "Code: %s" ' % (code, ))
+ ok(('BucketName: %s' % (bucket_name, )) in body, 'HTML should contain bucket name')
+
+def _website_request(bucket_name, path):
+ url = get_website_url('http', bucket_name, path)
+ print("url", url)
+
+ o = urlparse(url)
+ path = o.path + '?' + o.query
+ request_headers={}
+ request_headers['Host'] = o.hostname
+ res = _make_raw_request(config.main.host, config.main.port, 'GET', path, request_headers=request_headers, secure=False)
+ for (k,v) in res.getheaders():
+ print(k,v)
+ return res
+
+# ---------- Non-existant buckets via the website endpoint
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-existant bucket via website endpoint should give NoSuchBucket, exposing security risk')
+@attr('s3website')
+@attr('fails_on_rgw')
+def test_website_nonexistant_bucket_s3():
+ bucket_name = get_new_bucket_name()
+ res = _website_request(bucket_name, '')
+ _website_expected_error_response(res, bucket_name, 404, 'Not Found', 'NoSuchBucket')
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-existant bucket via website endpoint should give Forbidden, keeping bucket identity secure')
+@attr('s3website')
+@attr('fails_on_s3')
+def test_website_nonexistant_bucket_rgw():
+ bucket_name = get_new_bucket_name()
+ res = _website_request(bucket_name, '')
+ _website_expected_error_response(res, bucket_name, 403, 'Forbidden', 'AccessDenied')
+
+#------------- IndexDocument only, successes
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty public buckets via s3website return page for /, where page is public')
+@attr('s3website')
+def test_website_public_bucket_list_public_index():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDoc'])
+ bucket.make_public()
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.make_public()
+
+ res = _website_request(bucket.name, '')
+ body = res.read()
+ print(body)
+ eq(body, indexstring, 'default content should match index.html set content')
+ __website_expected_reponse_status(res, 200, 'OK')
+ indexhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty private buckets via s3website return page for /, where page is private')
+@attr('s3website')
+def test_website_private_bucket_list_public_index():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDoc'])
+ bucket.set_canned_acl('private')
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.make_public()
+
+ res = _website_request(bucket.name, '')
+ __website_expected_reponse_status(res, 200, 'OK')
+ body = res.read()
+ print(body)
+ eq(body, indexstring, 'default content should match index.html set content')
+ indexhtml.delete()
+ bucket.delete()
+
+
+# ---------- IndexDocument only, failures
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty private buckets via s3website return a 403 for /')
+@attr('s3website')
+def test_website_private_bucket_list_empty():
+ bucket = get_new_bucket()
+ bucket.set_canned_acl('private')
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDoc'])
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty public buckets via s3website return a 404 for /')
+@attr('s3website')
+def test_website_public_bucket_list_empty():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDoc'])
+ bucket.make_public()
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 404, 'Not Found', 'NoSuchKey')
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty public buckets via s3website return page for /, where page is private')
+@attr('s3website')
+def test_website_public_bucket_list_private_index():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDoc'])
+ bucket.make_public()
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ indexhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty private buckets via s3website return page for /, where page is private')
+@attr('s3website')
+def test_website_private_bucket_list_private_index():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDoc'])
+ bucket.set_canned_acl('private')
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+
+ indexhtml.delete()
+ bucket.delete()
+
+# ---------- IndexDocument & ErrorDocument, failures due to errordoc assigned but missing
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty private buckets via s3website return a 403 for /, missing errordoc')
+@attr('s3website')
+def test_website_private_bucket_list_empty_missingerrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ body = res.read()
+ print(body)
+
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty public buckets via s3website return a 404 for /, missing errordoc')
+@attr('s3website')
+def test_website_public_bucket_list_empty_missingerrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.make_public()
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 404, 'Not Found', 'NoSuchKey')
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty public buckets via s3website return page for /, where page is private, missing errordoc')
+@attr('s3website')
+def test_website_public_bucket_list_private_index_missingerrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.make_public()
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+
+ indexhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty private buckets via s3website return page for /, where page is private, missing errordoc')
+@attr('s3website')
+def test_website_private_bucket_list_private_index_missingerrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.set_canned_acl('private')
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+
+ indexhtml.delete()
+ bucket.delete()
+
+# ---------- IndexDocument & ErrorDocument, failures due to errordoc assigned but not accessible
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty private buckets via s3website return a 403 for /, blocked errordoc')
+@attr('s3website')
+def test_website_private_bucket_list_empty_blockederrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.set_canned_acl('private')
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ body = res.read()
+ print(body)
+ ok(errorstring not in body, 'error content should match error.html set content')
+
+ errorhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty public buckets via s3website return a 404 for /, blocked errordoc')
+@attr('s3website')
+def test_website_public_bucket_list_empty_blockederrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.make_public()
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 404, 'Not Found', 'NoSuchKey')
+ body = res.read()
+ print(body)
+ ok(errorstring not in body, 'error content should match error.html set content')
+
+ errorhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty public buckets via s3website return page for /, where page is private, blocked errordoc')
+@attr('s3website')
+def test_website_public_bucket_list_private_index_blockederrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.make_public()
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ body = res.read()
+ print(body)
+ ok(errorstring not in body, 'error content should match error.html set content')
+
+ indexhtml.delete()
+ errorhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty private buckets via s3website return page for /, where page is private, blocked errordoc')
+@attr('s3website')
+def test_website_private_bucket_list_private_index_blockederrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.set_canned_acl('private')
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('private')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ body = res.read()
+ print(body)
+ ok(errorstring not in body, 'error content should match error.html set content')
+
+ indexhtml.delete()
+ errorhtml.delete()
+ bucket.delete()
+
+# ---------- IndexDocument & ErrorDocument, failures with errordoc available
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty private buckets via s3website return a 403 for /, good errordoc')
+@attr('s3website')
+def test_website_private_bucket_list_empty_gooderrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.set_canned_acl('private')
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('public-read')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ body = res.read()
+ print(body)
+ eq(body, errorstring, 'error content should match error.html set content')
+
+ errorhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='empty public buckets via s3website return a 404 for /, good errordoc')
+@attr('s3website')
+def test_website_public_bucket_list_empty_gooderrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.make_public()
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('public-read')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 404, 'Not Found', 'NoSuchKey')
+ body = res.read()
+ print(body)
+ eq(body, errorstring, 'error content should match error.html set content')
+
+ errorhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty public buckets via s3website return page for /, where page is private')
+@attr('s3website')
+def test_website_public_bucket_list_private_index_gooderrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.make_public()
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('public-read')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ body = res.read()
+ print(body)
+ eq(body, errorstring, 'error content should match error.html set content')
+
+ indexhtml.delete()
+ errorhtml.delete()
+ bucket.delete()
+
+@attr(resource='bucket')
+@attr(method='get')
+@attr(operation='list')
+@attr(assertion='non-empty private buckets via s3website return page for /, where page is private')
+@attr('s3website')
+def test_website_private_bucket_list_private_index_gooderrordoc():
+ bucket = get_new_bucket()
+ indexname, errorname = _test_website_prep(bucket, WEBSITE_CONFIGS_XMLFRAG['IndexDocErrorDoc'])
+ bucket.set_canned_acl('private')
+ indexhtml = bucket.new_key(indexname)
+ indexstring = choose_bucket_prefix(template='{random}', max_len=256)
+ indexhtml.set_contents_from_string(indexstring)
+ indexhtml.set_canned_acl('private')
+ errorhtml = bucket.new_key(errorname)
+ errorstring = choose_bucket_prefix(template='{random}', max_len=256)
+ errorhtml.set_contents_from_string(errorstring)
+ errorhtml.set_canned_acl('public-read')
+
+ res = _website_request(bucket.name, '')
+ _website_expected_error_response(res, bucket.name, 403, 'Forbidden', 'AccessDenied')
+ body = res.read()
+ print(body)
+ eq(body, errorstring, 'error content should match error.html set content')
+
+ indexhtml.delete()
+ errorhtml.delete()
+ bucket.delete()