Files
simples3/simples3.go
T
ys-ll 9210db974c feat: add UseVirtualHostedStyle flag for S3-compatible services
S3-compatible services such as Alibaba Cloud OSS, Tencent COS and
Huawei OBS reject path-style URLs with 'SecondLevelDomainForbidden'
and only accept virtual-hosted style (https://bucket.endpoint/key).

This adds a UseVirtualHostedStyle field and SetVirtualHostedStyle()
setter. When enabled and a custom Endpoint is set, the first path
segment is treated as the bucket and prepended to the host as a
subdomain. AWS S3 and MinIO accept both styles, so this is a no-op
for them.

Includes tests for OSS-style endpoint, http/https scheme preservation
and the empty-path fallback used by ListBuckets-style operations.
2026-08-01 23:42:15 +08:00

163 lines
4.2 KiB
Go

// LICENSE BSD-2-Clause-FreeBSD
// Copyright (c) 2018, Rohan Verma <hello@rohanverma.net>
package simples3
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
const (
// AMZMetaPrefix to prefix metadata key.
AMZMetaPrefix = "x-amz-meta-"
)
// S3 provides a wrapper around your S3 credentials.
type S3 struct {
AccessKey string
SecretKey string
Region string
Client *http.Client
Token string
Endpoint string
URIFormat string
// UseVirtualHostedStyle enables virtual-hosted style addressing when a
// custom Endpoint is set. In virtual-hosted style the bucket name is
// embedded as a subdomain of the endpoint
// (e.g. https://mybucket.oss-cn-hangzhou.aliyuncs.com/key) instead of
// appearing in the URL path (https://oss-cn-hangzhou.aliyuncs.com/mybucket/key).
// S3-compatible services such as Alibaba Cloud OSS, Tencent COS and
// some private clouds reject path-style requests with
// "SecondLevelDomainForbidden", so this flag is required for them.
UseVirtualHostedStyle bool
initMode string
expiry time.Time
mu sync.Mutex
}
// New returns an instance of S3.
func New(region, accessKey, secretKey string) *S3 {
return &S3{
Region: region,
AccessKey: accessKey,
SecretKey: secretKey,
URIFormat: "https://s3.%s.amazonaws.com/%s",
}
}
func (s3 *S3) getClient() *http.Client {
if s3.Client == nil {
return http.DefaultClient
}
return s3.Client
}
// SetEndpoint can be used to the set a custom endpoint for
// using an alternate instance compatible with the s3 API.
// If no protocol is included in the URI, defaults to HTTPS.
func (s3 *S3) SetEndpoint(uri string) *S3 {
if len(uri) > 0 {
if !strings.HasPrefix(uri, "http") {
uri = "https://" + uri
}
// make sure there is no trailing slash
if uri[len(uri)-1] == '/' {
uri = uri[:len(uri)-1]
}
s3.Endpoint = uri
}
return s3
}
// SetToken can be used to set a Temporary Security Credential token obtained from
// using an IAM role or AWS STS.
func (s3 *S3) SetToken(token string) *S3 {
if token != "" {
s3.Token = token
}
return s3
}
// SetClient can be used to set the http client to be
// used by the package. If client passed is nil,
// http.DefaultClient is used.
func (s3 *S3) SetClient(client *http.Client) *S3 {
if client != nil {
s3.Client = client
} else {
s3.Client = http.DefaultClient
}
return s3
}
// SetVirtualHostedStyle toggles virtual-hosted style URL addressing for S3
// operations against a custom Endpoint. When enabled, the bucket name is
// embedded as a subdomain of the endpoint
// (https://mybucket.<endpoint>/<key>). S3-compatible services that reject
// path-style URLs (Alibaba Cloud OSS, Tencent COS, Huawei OBS, etc.) need
// this enabled. AWS S3 supports both styles; this is a no-op when no custom
// Endpoint is set.
func (s3 *S3) SetVirtualHostedStyle(enabled bool) *S3 {
s3.UseVirtualHostedStyle = enabled
return s3
}
func (s3 *S3) signRequest(req *http.Request) error {
var (
err error
date = req.Header.Get("Date")
t = time.Now().UTC()
)
if date != "" {
t, err = time.Parse(http.TimeFormat, date)
if err != nil {
return err
}
}
req.Header.Set("Date", t.Format(amzDateISO8601TimeFormat))
req.Header.Set("X-Amz-Date", t.Format(amzDateISO8601TimeFormat))
if s3.Token != "" {
req.Header.Set("X-Amz-Security-Token", s3.Token)
}
// The x-amz-content-sha256 header is required for all AWS
// Signature Version 4 requests. It provides a hash of the
// request payload. If there is no payload, you must provide
// the hash of an empty string.
if req.Header.Get("x-amz-content-sha256") == "" {
emptyhash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
req.Header.Set("x-amz-content-sha256", emptyhash)
}
k := s3.signKeys(t)
h := hmac.New(sha256.New, k)
s3.writeStringToSign(h, t, req)
auth := bytes.NewBufferString(algorithm)
auth.Write([]byte(" Credential=" + s3.AccessKey + "/" + s3.creds(t)))
auth.Write([]byte{',', ' '})
auth.Write([]byte("SignedHeaders="))
writeHeaderList(auth, req)
auth.Write([]byte{',', ' '})
auth.Write([]byte("Signature=" + fmt.Sprintf("%x", h.Sum(nil))))
req.Header.Set("Authorization", auth.String())
return nil
}