Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,32 @@ s3 := simples3.New("nyc3", "your-access-key", "your-secret-key")
s3.SetEndpoint("https://nyc3.digitaloceanspaces.com")
```

### Addressing Style

`Bucket` remains required for bucket-scoped operations. Addressing style is a client setting, not a bucketless API mode.

By default, simples3 preserves its legacy behavior for compatibility:
- runtime requests use path-style URLs
- presigned URLs keep their historical default behavior
- direct `CreateUploadPolicies()` defaults keep their historical action URL behavior

To explicitly control addressing style across supported surfaces, use `SetUsePathStyle`:

```go
// Force path-style addressing
s3 := simples3.New("us-east-1", "your-access-key", "your-secret-key")
s3.SetUsePathStyle(true)

// Opt into virtual-hosted-style addressing when the bucket/endpoint is compatible
s3.SetUsePathStyle(false)
```

When virtual-hosted-style is explicitly enabled, simples3 safely falls back to path-style for incompatible cases such as:
- dotted bucket names over HTTPS
- localhost or IP-based endpoints
- endpoints with a path prefix (for example `https://example.com/base`)
- non-DNS-compatible bucket names

### IAM Credentials

On EC2 instances, use IAM roles automatically:
Expand Down
237 changes: 237 additions & 0 deletions addressing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package simples3

import (
"fmt"
"net"
"net/url"
"strings"
)

type addressingMode uint8

const (
addressingModeLegacy addressingMode = iota
addressingModePath
addressingModeVirtual
)

type addressingSurface uint8

const (
addressingSurfaceRuntime addressingSurface = iota
addressingSurfacePresign
addressingSurfacePolicy
)

type addressStyle uint8

const (
addressStylePath addressStyle = iota
addressStyleVirtual
)

type resolvedAddress struct {
scheme string
host string
path string
style addressStyle
fallbackReason string
}

func (a resolvedAddress) urlString() string {
path := a.path
if path == "" {
path = "/"
}
return a.scheme + "://" + a.host + path
}

func (s3 *S3) resolveAddress(surface addressingSurface, bucket string, args ...string) resolvedAddress {
switch s3.addressingMode {
case addressingModePath:
return s3.resolvePathAddress(bucket, args...)
case addressingModeVirtual:
return s3.resolveExplicitVirtualAddress(bucket, args...)
default:
return s3.resolveLegacyAddress(surface, bucket, args...)
}
}

func (s3 *S3) resolveLegacyAddress(surface addressingSurface, bucket string, args ...string) resolvedAddress {
switch surface {
case addressingSurfacePresign:
if endpoint, _ := url.Parse(s3.Endpoint); endpoint.Host != "" {
return s3.resolvePathAddress(bucket, args...)
}
return resolvedAddress{
scheme: "https",
host: bucket + "." + defaultPresignedHost,
path: buildVirtualObjectPath("", args...),
style: addressStyleVirtual,
}
case addressingSurfacePolicy:
return parseResolvedAddress(fmt.Sprintf(defaultUploadURLFormat, bucket), addressStyleVirtual, "")
default:
return s3.resolvePathAddress(bucket, args...)
}
}

func (s3 *S3) resolvePathAddress(bucket string, args ...string) resolvedAddress {
return parseResolvedAddress(s3.legacyPathURL(bucket, args...), addressStylePath, "")
}

func (s3 *S3) resolveExplicitVirtualAddress(bucket string, args ...string) resolvedAddress {
base, err := s3.serviceBaseURL()
if err != nil {
return parseResolvedAddress(s3.legacyPathURL(bucket, args...), addressStylePath, "invalid service endpoint")
}

if reason := virtualAddressingFallbackReason(base, bucket); reason != "" {
return parseResolvedAddress(s3.legacyPathURL(bucket, args...), addressStylePath, reason)
}

return resolvedAddress{
scheme: base.Scheme,
host: bucketHost(base, bucket),
path: buildVirtualObjectPath(base.EscapedPath(), args...),
style: addressStyleVirtual,
}
}

func (s3 *S3) legacyPathURL(bucket string, args ...string) string {
path := bucket
if len(args) > 0 {
path += "/" + strings.Join(args, "/")
}
encodedPath := encodePath(path)

if len(s3.Endpoint) > 0 {
return s3.Endpoint + "/" + encodedPath
}
return fmt.Sprintf(s3.URIFormat, s3.Region, encodedPath)
}

func (s3 *S3) serviceBaseURL() (*url.URL, error) {
rawURL := s3.Endpoint
if rawURL == "" {
rawURL = fmt.Sprintf(s3.URIFormat, s3.Region, "")
}
return url.Parse(rawURL)
}

func parseResolvedAddress(rawURL string, style addressStyle, fallbackReason string) resolvedAddress {
parsed, err := url.Parse(rawURL)
if err != nil {
return resolvedAddress{style: style, fallbackReason: fallbackReason}
}

path := parsed.EscapedPath()
if path == "" {
path = "/"
}

return resolvedAddress{
scheme: parsed.Scheme,
host: parsed.Host,
path: path,
style: style,
fallbackReason: fallbackReason,
}
}

func buildVirtualObjectPath(basePath string, args ...string) string {
trimmedBase := strings.TrimRight(basePath, "/")
objectPath := strings.Join(args, "/")
if objectPath == "" {
if trimmedBase == "" {
return "/"
}
return trimmedBase + "/"
}

encodedObjectPath := encodePath(objectPath)
if trimmedBase == "" {
return "/" + encodedObjectPath
}
return trimmedBase + "/" + encodedObjectPath
}

func bucketHost(base *url.URL, bucket string) string {
hostname := base.Hostname()
if port := base.Port(); port != "" {
return bucket + "." + hostname + ":" + port
}
return bucket + "." + hostname
}

func virtualAddressingFallbackReason(base *url.URL, bucket string) string {
if !dnsCompatibleBucketName(bucket) {
return "bucket is not DNS compatible"
}
if base.Scheme == "https" && strings.Contains(bucket, ".") {
return "dotted bucket over https"
}
if basePath := strings.Trim(base.EscapedPath(), "/"); basePath != "" {
return "endpoint has path prefix"
}
if hostname := base.Hostname(); hostname == "localhost" {
return "localhost endpoint"
} else if hostname != "" {
if ip := net.ParseIP(hostname); ip != nil {
return "ip endpoint"
}
}
return ""
}

func dnsCompatibleBucketName(bucket string) bool {
if len(bucket) < 3 || len(bucket) > 63 {
return false
}
if strings.Contains(bucket, "..") {
return false
}
if !isLowercaseLetterOrDigit(bucket[0]) || !isLowercaseLetterOrDigit(bucket[len(bucket)-1]) {
return false
}
for i := 1; i < len(bucket)-1; i++ {
c := bucket[i]
if !isLowercaseLetterOrDigit(c) && c != '.' && c != '-' {
return false
}
}

parts := strings.Split(bucket, ".")
for _, part := range parts {
if part == "" {
return false
}
if !isLowercaseLetterOrDigit(part[0]) || !isLowercaseLetterOrDigit(part[len(part)-1]) {
return false
}
}

if len(parts) == 4 {
isIPAddress := true
for _, part := range parts {
for i := 0; i < len(part); i++ {
if part[i] < '0' || part[i] > '9' {
isIPAddress = false
break
}
}
if !isIPAddress {
break
}
}
if isIPAddress {
return false
}
}

return true
}

func isLowercaseLetterOrDigit(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
}
Loading
Loading