mirror of
https://github.com/versity/versitygw.git
synced 2026-07-02 16:54:25 +00:00
f08f76fea4
Integrate x-amz-website-redirect-location across object metadata flows so uploads, copies, multipart creation, HEAD, and GET preserve and return redirect locations, and website hosting applies object-level redirects from the stored value.
865 lines
26 KiB
Go
865 lines
26 KiB
Go
// Copyright 2026 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
// WebsiteHosting_error_document_served tests that a missing website object
|
|
// serves the configured error document while preserving the original 404 status.
|
|
func WebsiteHosting_error_document_served(s *S3Conf) error {
|
|
testName := "WebsiteHosting_error_document_served"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
ErrorDocument: &types.ErrorDocument{
|
|
Key: getPtr("error.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
errorContent := "<html><body>Custom Error Page</body></html>"
|
|
_, err = putObjectWithData(int64(len(errorContent)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("error.html"),
|
|
Body: strings.NewReader(errorContent),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "nonexistent-key", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if got := resp.Header.Get("Content-Type"); got != "text/html" {
|
|
return fmt.Errorf("expected text/html Content-Type, got %q", got)
|
|
}
|
|
return checkWebsiteResponse(resp, http.StatusNotFound, []byte(errorContent))
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_error_document_not_found tests that a missing configured
|
|
// error document returns the complete website NoSuchKey error response.
|
|
func WebsiteHosting_error_document_not_found(s *S3Conf) error {
|
|
testName := "WebsiteHosting_error_document_not_found"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
ErrorDocument: &types.ErrorDocument{
|
|
Key: getPtr("error.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "nonexistent-key", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkWebsiteErrorResponse(resp, s3err.GetAPIError(s3err.ErrNoSuchKey))
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_no_error_document tests that a website bucket without an
|
|
// error document returns the complete website NoSuchKey error response.
|
|
func WebsiteHosting_no_error_document(s *S3Conf) error {
|
|
testName := "WebsiteHosting_no_error_document"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "nonexistent-key", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkWebsiteErrorResponse(resp, s3err.GetAPIError(s3err.ErrNoSuchKey))
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_no_bucket_in_request_location tests that website endpoint
|
|
// requests that cannot resolve a bucket still include a useful Location header.
|
|
func WebsiteHosting_no_bucket_in_request_location(s *S3Conf) error {
|
|
testName := "WebsiteHosting_no_bucket_in_request_location"
|
|
return actionHandlerNoSetup(s, testName, func(_ *s3.Client, _ string) error {
|
|
_, domain, port := websiteEndpointParts(s)
|
|
badHost := "nested.bucket." + domain
|
|
baseHost := domain
|
|
if port != "" {
|
|
badHost = fmt.Sprintf("%s:%s", badHost, port)
|
|
baseHost = fmt.Sprintf("%s:%s", baseHost, port)
|
|
}
|
|
|
|
reqURL, err := websiteAbsoluteURL(s, badHost, "/")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create website request: %w", err)
|
|
}
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wantLocation, err := websiteAbsoluteURL(s, baseHost, "/")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if got := resp.Header.Get("Location"); got != wantLocation {
|
|
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
|
}
|
|
|
|
return checkWebsiteErrorResponse(resp, s3err.GetAPIError(s3err.ErrNoBucketInRequest))
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_private_object_and_error_document tests that website hosting
|
|
// does not serve either the requested object or the configured error document
|
|
// unless public object access has been granted.
|
|
func WebsiteHosting_private_object_and_error_document(s *S3Conf) error {
|
|
testName := "WebsiteHosting_private_object_and_error_document"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
ErrorDocument: &types.ErrorDocument{
|
|
Key: getPtr("error.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
privateError := "private error"
|
|
_, err = putObjectWithData(int64(len(privateError)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("error.html"),
|
|
Body: strings.NewReader(privateError),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "private.html", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkWebsiteErrorResponse(resp, s3err.GetAPIError(s3err.ErrAccessDenied))
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_routing_rule_post_request_redirect tests that a post-request
|
|
// routing rule matching a 404 issues a redirect instead of serving an error.
|
|
func WebsiteHosting_routing_rule_post_request_redirect(s *S3Conf) error {
|
|
testName := "WebsiteHosting_routing_rule_post_request_redirect"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
ErrorDocument: &types.ErrorDocument{
|
|
Key: getPtr("error.html"),
|
|
},
|
|
RoutingRules: []types.RoutingRule{
|
|
{
|
|
Condition: &types.Condition{
|
|
HttpErrorCodeReturnedEquals: getPtr("404"),
|
|
},
|
|
Redirect: &types.Redirect{
|
|
HostName: getPtr("fallback.example.com"),
|
|
ReplaceKeyWith: getPtr("not-found"),
|
|
HttpRedirectCode: getPtr("302"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "missing-page", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wantLocation, err := websiteAbsoluteURL(s, "fallback.example.com", "not-found")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if got := resp.Header.Get("Location"); got != wantLocation {
|
|
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
|
}
|
|
return checkWebsiteResponse(resp, http.StatusFound, nil)
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_routing_rule_pre_request_redirect tests that a key-prefix
|
|
// routing rule redirects before public access or object existence is checked.
|
|
func WebsiteHosting_routing_rule_pre_request_redirect(s *S3Conf) error {
|
|
testName := "WebsiteHosting_routing_rule_pre_request_redirect"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
RoutingRules: []types.RoutingRule{
|
|
{
|
|
Condition: &types.Condition{
|
|
KeyPrefixEquals: getPtr("old-docs/"),
|
|
},
|
|
Redirect: &types.Redirect{
|
|
ReplaceKeyPrefixWith: getPtr("new-docs/"),
|
|
HttpRedirectCode: getPtr("301"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "old-docs/page.html", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
wantLocation, err := websiteURL(s, bucket, "new-docs/page.html")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if got := resp.Header.Get("Location"); got != wantLocation {
|
|
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
|
}
|
|
return checkWebsiteResponse(resp, http.StatusMovedPermanently, nil)
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_routing_rule_prefix_and_error_redirect tests a routing rule
|
|
// with both KeyPrefixEquals and HttpErrorCodeReturnedEquals conditions.
|
|
func WebsiteHosting_routing_rule_prefix_and_error_redirect(s *S3Conf) error {
|
|
testName := "WebsiteHosting_routing_rule_prefix_and_error_redirect"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
ErrorDocument: &types.ErrorDocument{
|
|
Key: getPtr("error.html"),
|
|
},
|
|
RoutingRules: []types.RoutingRule{
|
|
{
|
|
Condition: &types.Condition{
|
|
KeyPrefixEquals: getPtr("old/"),
|
|
HttpErrorCodeReturnedEquals: getPtr("404"),
|
|
},
|
|
Redirect: &types.Redirect{
|
|
ReplaceKeyPrefixWith: getPtr("archived/"),
|
|
HttpRedirectCode: getPtr("307"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "old/missing.html?ref=1", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
wantLocation, err := websiteURL(s, bucket, "archived/missing.html?ref=1")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if got := resp.Header.Get("Location"); got != wantLocation {
|
|
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
|
}
|
|
return checkWebsiteResponse(resp, http.StatusTemporaryRedirect, nil)
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_routing_rule_no_match_serves_error_document tests that routing
|
|
// rules which do not match fall back to the configured error document.
|
|
func WebsiteHosting_routing_rule_no_match_serves_error_document(s *S3Conf) error {
|
|
testName := "WebsiteHosting_routing_rule_no_match_serves_error_document"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
ErrorDocument: &types.ErrorDocument{
|
|
Key: getPtr("error.html"),
|
|
},
|
|
RoutingRules: []types.RoutingRule{
|
|
{
|
|
Condition: &types.Condition{
|
|
KeyPrefixEquals: getPtr("docs/"),
|
|
HttpErrorCodeReturnedEquals: getPtr("404"),
|
|
},
|
|
Redirect: &types.Redirect{
|
|
ReplaceKeyPrefixWith: getPtr("archive/"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
errorContent := "<html><body>fallback error</body></html>"
|
|
_, err = putObjectWithData(int64(len(errorContent)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("error.html"),
|
|
Body: strings.NewReader(errorContent),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "images/missing.png", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return checkWebsiteResponse(resp, http.StatusNotFound, []byte(errorContent))
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_redirect_all_requests tests RedirectAllRequestsTo, including
|
|
// path and query preservation, without requiring public object access.
|
|
func WebsiteHosting_redirect_all_requests(s *S3Conf) error {
|
|
testName := "WebsiteHosting_redirect_all_requests"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
RedirectAllRequestsTo: &types.RedirectAllRequestsTo{
|
|
HostName: getPtr("www.example.com"),
|
|
Protocol: types.ProtocolHttps,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "any/path/here?tracking=1", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if got, want := resp.Header.Get("Location"), "https://www.example.com/any/path/here?tracking=1"; got != want {
|
|
return fmt.Errorf("expected Location %q, got %q", want, got)
|
|
}
|
|
return checkWebsiteResponse(resp, http.StatusMovedPermanently, nil)
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_object_redirect_location tests that an object-level website
|
|
// redirect emits a 301 with the stored Location after object fetch succeeds.
|
|
func WebsiteHosting_object_redirect_location(s *S3Conf) error {
|
|
testName := "WebsiteHosting_object_redirect_location"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
redirectLocation := "/new-page.html"
|
|
objectBody := "old"
|
|
_, err = putObjectWithData(int64(len(objectBody)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("old-page.html"),
|
|
Body: strings.NewReader(objectBody),
|
|
ContentType: getPtr("text/html"),
|
|
WebsiteRedirectLocation: &redirectLocation,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "old-page.html", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if got := resp.Header.Get("Location"); got != redirectLocation {
|
|
return fmt.Errorf("expected Location %q, got %q", redirectLocation, got)
|
|
}
|
|
return checkWebsiteResponse(resp, http.StatusMovedPermanently, nil)
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_index_document tests root and directory-style index document
|
|
// resolution through the website endpoint.
|
|
func WebsiteHosting_index_document(s *S3Conf) error {
|
|
testName := "WebsiteHosting_index_document"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
indexContent := "<html><body>Welcome</body></html>"
|
|
_, err = putObjectWithData(int64(len(indexContent)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("index.html"),
|
|
Body: strings.NewReader(indexContent),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
docsContent := "<html><body>Docs Home</body></html>"
|
|
_, err = putObjectWithData(int64(len(docsContent)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("docs/index.html"),
|
|
Body: strings.NewReader(docsContent),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
path string
|
|
body string
|
|
}{
|
|
{"/", indexContent},
|
|
{"docs/", docsContent},
|
|
} {
|
|
resp, err := websiteGet(s, bucket, test.path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = checkWebsiteResponse(resp, http.StatusOK, []byte(test.body))
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", test.path, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WebsiteHosting_index_error_document_and_routing_rules covers a combined
|
|
// website configuration with index, error document, pre-rule, and post-rule.
|
|
func WebsiteHosting_index_error_document_and_routing_rules(s *S3Conf) error {
|
|
testName := "WebsiteHosting_index_error_document_and_routing_rules"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
ErrorDocument: &types.ErrorDocument{
|
|
Key: getPtr("error.html"),
|
|
},
|
|
RoutingRules: []types.RoutingRule{
|
|
{
|
|
Condition: &types.Condition{
|
|
KeyPrefixEquals: getPtr("legacy/"),
|
|
},
|
|
Redirect: &types.Redirect{
|
|
ReplaceKeyPrefixWith: getPtr("docs/"),
|
|
HttpRedirectCode: getPtr("301"),
|
|
},
|
|
},
|
|
{
|
|
Condition: &types.Condition{
|
|
HttpErrorCodeReturnedEquals: getPtr("404"),
|
|
},
|
|
Redirect: &types.Redirect{
|
|
HostName: getPtr("fallback.example.com"),
|
|
ReplaceKeyWith: getPtr("missing"),
|
|
HttpRedirectCode: getPtr("302"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
indexContent := "<html><body>combined index</body></html>"
|
|
_, err = putObjectWithData(int64(len(indexContent)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("index.html"),
|
|
Body: strings.NewReader(indexContent),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
combinedError := "combined error"
|
|
_, err = putObjectWithData(int64(len(combinedError)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("error.html"),
|
|
Body: strings.NewReader(combinedError),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
indexResp, err := websiteGet(s, bucket, "/", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := checkWebsiteResponse(indexResp, http.StatusOK, []byte(indexContent)); err != nil {
|
|
return err
|
|
}
|
|
indexResp.Body.Close()
|
|
|
|
preResp, err := websiteGet(s, bucket, "legacy/page.html", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wantPreLocation, err := websiteURL(s, bucket, "docs/page.html")
|
|
if err != nil {
|
|
preResp.Body.Close()
|
|
return err
|
|
}
|
|
if got := preResp.Header.Get("Location"); got != wantPreLocation {
|
|
preResp.Body.Close()
|
|
return fmt.Errorf("expected pre-rule Location %q, got %q", wantPreLocation, got)
|
|
}
|
|
if err := checkWebsiteResponse(preResp, http.StatusMovedPermanently, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
postResp, err := websiteGet(s, bucket, "unknown.html", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wantPostLocation, err := websiteAbsoluteURL(s, "fallback.example.com", "missing")
|
|
if err != nil {
|
|
postResp.Body.Close()
|
|
return err
|
|
}
|
|
if got := postResp.Header.Get("Location"); got != wantPostLocation {
|
|
postResp.Body.Close()
|
|
return fmt.Errorf("expected post-rule Location %q, got %q", wantPostLocation, got)
|
|
}
|
|
if err := checkWebsiteResponse(postResp, http.StatusFound, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func WebsiteHosting_options_preflight_access_granted(s *S3Conf) error {
|
|
testName := "WebsiteHosting_options_preflight_access_granted"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
|
|
Bucket: &bucket,
|
|
CORSConfiguration: &types.CORSConfiguration{
|
|
CORSRules: []types.CORSRule{
|
|
{
|
|
AllowedOrigins: []string{"https://client.example"},
|
|
AllowedMethods: []string{http.MethodGet, http.MethodHead},
|
|
AllowedHeaders: []string{"Content-Type", "X-Amz-Date"},
|
|
ExposeHeaders: []string{"Content-Length"},
|
|
MaxAgeSeconds: getPtr(int32(42)),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteOptions(s, bucket, "index.html", map[string]string{
|
|
"Origin": "https://client.example",
|
|
"Access-Control-Request-Method": http.MethodGet,
|
|
"Access-Control-Request-Headers": "content-type, X-Amz-Date",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
corsHeaders, err := extractCORSHeaders(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := comparePreflightResult(&PreflightResult{
|
|
Origin: "https://client.example",
|
|
Methods: "GET, HEAD",
|
|
AllowHeaders: "content-type, x-amz-date",
|
|
ExposeHeaders: "Content-Length",
|
|
MaxAge: "42",
|
|
AllowCredentials: "true",
|
|
Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
|
|
}, corsHeaders); err != nil {
|
|
return err
|
|
}
|
|
|
|
return checkWebsiteResponse(resp, http.StatusOK, nil)
|
|
})
|
|
}
|
|
|
|
func WebsiteHosting_get_cors_headers(s *S3Conf) error {
|
|
testName := "WebsiteHosting_get_cors_headers"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
indexContent := "<html><body>CORS GET</body></html>"
|
|
_, err = putObjectWithData(int64(len(indexContent)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("index.html"),
|
|
Body: strings.NewReader(indexContent),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
maxAge := int32(42)
|
|
err = putBucketCors(s3client, &s3.PutBucketCorsInput{
|
|
Bucket: &bucket,
|
|
CORSConfiguration: &types.CORSConfiguration{
|
|
CORSRules: []types.CORSRule{
|
|
{
|
|
AllowedOrigins: []string{"https://client.example"},
|
|
AllowedMethods: []string{http.MethodGet, http.MethodHead},
|
|
ExposeHeaders: []string{"Content-Length"},
|
|
MaxAgeSeconds: &maxAge,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteGet(s, bucket, "/", map[string]string{
|
|
"Origin": "https://client.example",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
corsHeaders, err := extractCORSHeaders(resp)
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return err
|
|
}
|
|
if err := comparePreflightResult(&PreflightResult{
|
|
Origin: "https://client.example",
|
|
Methods: "GET, HEAD",
|
|
ExposeHeaders: "Content-Length, ETag, x-amz-storage-class",
|
|
MaxAge: "42",
|
|
AllowCredentials: "true",
|
|
Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
|
|
}, corsHeaders); err != nil {
|
|
resp.Body.Close()
|
|
return err
|
|
}
|
|
|
|
return checkWebsiteResponse(resp, http.StatusOK, []byte(indexContent))
|
|
})
|
|
}
|
|
|
|
func WebsiteHosting_head_cors_headers(s *S3Conf) error {
|
|
testName := "WebsiteHosting_head_cors_headers"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
|
IndexDocument: &types.IndexDocument{
|
|
Suffix: getPtr("index.html"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
|
return err
|
|
}
|
|
|
|
headContent := "<html><body>CORS HEAD</body></html>"
|
|
_, err = putObjectWithData(int64(len(headContent)), &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: getPtr("head.html"),
|
|
Body: strings.NewReader(headContent),
|
|
ContentType: getPtr("text/html"),
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = putBucketCors(s3client, &s3.PutBucketCorsInput{
|
|
Bucket: &bucket,
|
|
CORSConfiguration: &types.CORSConfiguration{
|
|
CORSRules: []types.CORSRule{
|
|
{
|
|
AllowedOrigins: []string{"*"},
|
|
AllowedMethods: []string{http.MethodHead},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteHead(s, bucket, "head.html", map[string]string{
|
|
"Origin": "https://client.example",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
corsHeaders, err := extractCORSHeaders(resp)
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return err
|
|
}
|
|
if err := comparePreflightResult(&PreflightResult{
|
|
Origin: "*",
|
|
Methods: "HEAD",
|
|
ExposeHeaders: "ETag, x-amz-storage-class",
|
|
AllowCredentials: "false",
|
|
Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
|
|
}, corsHeaders); err != nil {
|
|
resp.Body.Close()
|
|
return err
|
|
}
|
|
|
|
return checkWebsiteResponse(resp, http.StatusOK, nil)
|
|
})
|
|
}
|
|
|
|
func WebsiteHosting_options_preflight_access_forbidden(s *S3Conf) error {
|
|
testName := "WebsiteHosting_options_preflight_access_forbidden"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
err := putBucketCors(s3client, &s3.PutBucketCorsInput{
|
|
Bucket: &bucket,
|
|
CORSConfiguration: &types.CORSConfiguration{
|
|
CORSRules: []types.CORSRule{
|
|
{
|
|
AllowedOrigins: []string{"https://client.example"},
|
|
AllowedMethods: []string{http.MethodHead},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := websiteOptions(s, bucket, "index.html", map[string]string{
|
|
"Origin": "https://client.example",
|
|
"Access-Control-Request-Method": http.MethodGet,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return checkWebsiteErrorResponse(resp,
|
|
s3err.GetAccessForbiddenErr(s3err.ErrCORSForbidden, http.MethodOptions, s3err.ResourceTypeObject))
|
|
})
|
|
}
|
|
|
|
func WebsiteHosting_options_preflight_missing_origin(s *S3Conf) error {
|
|
testName := "WebsiteHosting_options_preflight_missing_origin"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
resp, err := websiteOptions(s, bucket, "index.html", map[string]string{
|
|
"Access-Control-Request-Method": http.MethodGet,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return checkWebsiteErrorResponse(resp, s3err.GetAPIError(s3err.ErrMissingCORSOrigin))
|
|
})
|
|
}
|