mirror of
https://github.com/versity/versitygw.git
synced 2026-09-19 06:32:10 +00:00
feat: implements host-style bucket addressing in the gateway.
Closes #803 Implements host-style bucket addressing in the gateway. This feature can be enabled by running the gateway with the `--virtual-domain` flag and specifying a virtual domain name. Example: ```bash ./versitygw -a user -s secret --virtual-domain localhost:7070 posix /tmp/vgw ``` The implementation follows this approach: it introduces a middleware (`HostStyleParser`) that parses the bucket name from the `Host` header and appends it to the URL path. This effectively transforms the request into a path-style bucket addressing format, which the gateway already supports. With this design, the gateway can handle both path-style and host-style requests when running in host-style mode. For local testing, one can either set up a local DNS server to wildcard-match all subdomains of a specified domain and resolve them to the local IP address, or manually add entries to `/etc/hosts` to resolve bucket-prefixed hosts to the server IP (e.g., `127.0.0.1`).
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
// Copyright 2023 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 middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// HostStyleParser is a middleware which parses the bucket name
|
||||
// from the 'Host' header and appends in the request URL path
|
||||
func HostStyleParser(virtualDomain string) fiber.Handler {
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
host := string(ctx.Request().Host())
|
||||
// the host should match this pattern: '<bucket_name>.<virtual_domain>'
|
||||
bucket, _, found := strings.Cut(host, "."+virtualDomain)
|
||||
if !found || bucket == "" {
|
||||
return ctx.Next()
|
||||
}
|
||||
path := ctx.Path()
|
||||
pathStyleUrl := fmt.Sprintf("/%v%v", bucket, path)
|
||||
ctx.Path(pathStyleUrl)
|
||||
|
||||
return ctx.Next()
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
func DecodeURL(logger s3log.AuditLogger, mm *metrics.Manager) fiber.Handler {
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
unescp, err := url.QueryUnescape(string(ctx.Request().URI().PathOriginal()))
|
||||
unescp, err := url.PathUnescape(string(ctx.Request().URI().PathOriginal()))
|
||||
if err != nil {
|
||||
return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrInvalidURI), &controllers.MetaOpts{Logger: logger, MetricsMng: mm})
|
||||
}
|
||||
|
||||
+22
-9
@@ -29,15 +29,16 @@ import (
|
||||
)
|
||||
|
||||
type S3ApiServer struct {
|
||||
app *fiber.App
|
||||
backend backend.Backend
|
||||
router *S3ApiRouter
|
||||
port string
|
||||
cert *tls.Certificate
|
||||
quiet bool
|
||||
debug bool
|
||||
readonly bool
|
||||
health string
|
||||
app *fiber.App
|
||||
backend backend.Backend
|
||||
router *S3ApiRouter
|
||||
port string
|
||||
cert *tls.Certificate
|
||||
quiet bool
|
||||
debug bool
|
||||
readonly bool
|
||||
health string
|
||||
virtualDomain string
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -76,6 +77,13 @@ func New(
|
||||
})
|
||||
}
|
||||
app.Use(middlewares.DecodeURL(l, mm))
|
||||
|
||||
// initialize host-style parser in virtual domain is specified
|
||||
if server.virtualDomain != "" {
|
||||
app.Use(middlewares.HostStyleParser(server.virtualDomain))
|
||||
}
|
||||
|
||||
// initialize the debug logger in debug mode
|
||||
if server.debug {
|
||||
app.Use(middlewares.DebugLogger())
|
||||
}
|
||||
@@ -123,6 +131,11 @@ func WithReadOnly() Option {
|
||||
return func(s *S3ApiServer) { s.readonly = true }
|
||||
}
|
||||
|
||||
// WithHostStyle enabled host-style bucket addressing on the server
|
||||
func WithHostStyle(virtualDomain string) Option {
|
||||
return func(s *S3ApiServer) { s.virtualDomain = virtualDomain }
|
||||
}
|
||||
|
||||
func (sa *S3ApiServer) Serve() (err error) {
|
||||
if sa.cert != nil {
|
||||
return sa.app.ListenTLSWithCertificate(sa.port, *sa.cert)
|
||||
|
||||
+3
-80
@@ -29,7 +29,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/smithy-go/encoding/httpbinding"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/versity/versitygw/s3api/debuglogger"
|
||||
@@ -42,10 +41,6 @@ var (
|
||||
bucketNameIpRegexp = regexp.MustCompile(`^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`)
|
||||
)
|
||||
|
||||
const (
|
||||
upperhex = "0123456789ABCDEF"
|
||||
)
|
||||
|
||||
func GetUserMetaData(headers *fasthttp.RequestHeader) (metadata map[string]string) {
|
||||
metadata = make(map[string]string)
|
||||
headers.DisableNormalizing()
|
||||
@@ -71,9 +66,9 @@ func createHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, contentLength
|
||||
body = bytes.NewReader(req.Body())
|
||||
}
|
||||
|
||||
escapedURI := escapeOriginalURI(ctx)
|
||||
uri := ctx.OriginalURL()
|
||||
|
||||
httpReq, err := http.NewRequest(string(req.Header.Method()), escapedURI, body)
|
||||
httpReq, err := http.NewRequest(string(req.Header.Method()), uri, body)
|
||||
if err != nil {
|
||||
return nil, errors.New("error in creating an http request")
|
||||
}
|
||||
@@ -126,8 +121,7 @@ func createPresignedHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, cont
|
||||
body = bytes.NewReader(req.Body())
|
||||
}
|
||||
|
||||
uri := string(ctx.Request().URI().Path())
|
||||
uri = httpbinding.EscapePath(uri, false)
|
||||
uri, _, _ := strings.Cut(ctx.OriginalURL(), "?")
|
||||
isFirst := true
|
||||
|
||||
ctx.Request().URI().QueryArgs().VisitAll(func(key, value []byte) {
|
||||
@@ -400,77 +394,6 @@ func IsValidOwnership(val types.ObjectOwnership) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func escapeOriginalURI(ctx *fiber.Ctx) string {
|
||||
path := ctx.Path()
|
||||
|
||||
// Escape the URI original path
|
||||
escapedURI := escapePath(path)
|
||||
|
||||
// Add the URI query params
|
||||
query := string(ctx.Request().URI().QueryArgs().QueryString())
|
||||
if query != "" {
|
||||
escapedURI = escapedURI + "?" + query
|
||||
}
|
||||
|
||||
return escapedURI
|
||||
}
|
||||
|
||||
// Escapes the path string
|
||||
// Most of the parts copied from std url
|
||||
func escapePath(s string) string {
|
||||
hexCount := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if shouldEscape(c) {
|
||||
hexCount++
|
||||
}
|
||||
}
|
||||
|
||||
if hexCount == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
var buf [64]byte
|
||||
var t []byte
|
||||
|
||||
required := len(s) + 2*hexCount
|
||||
if required <= len(buf) {
|
||||
t = buf[:required]
|
||||
} else {
|
||||
t = make([]byte, required)
|
||||
}
|
||||
|
||||
j := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch c := s[i]; {
|
||||
case shouldEscape(c):
|
||||
t[j] = '%'
|
||||
t[j+1] = upperhex[c>>4]
|
||||
t[j+2] = upperhex[c&15]
|
||||
j += 3
|
||||
default:
|
||||
t[j] = s[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
return string(t)
|
||||
}
|
||||
|
||||
// Checks if the character needs to be escaped
|
||||
func shouldEscape(c byte) bool {
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
|
||||
return false
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '-', '_', '.', '~', '/':
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type ChecksumValues map[types.ChecksumAlgorithm]string
|
||||
|
||||
// Headers concatinates checksum algorithm by prefixing each
|
||||
|
||||
@@ -418,128 +418,6 @@ func TestIsValidOwnership(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_shouldEscape(t *testing.T) {
|
||||
type args struct {
|
||||
c byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "shouldn't-escape-alphanum",
|
||||
args: args{
|
||||
c: 'h',
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "shouldn't-escape-unreserved-char",
|
||||
args: args{
|
||||
c: '_',
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "shouldn't-escape-unreserved-number",
|
||||
args: args{
|
||||
c: '0',
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "shouldn't-escape-path-separator",
|
||||
args: args{
|
||||
c: '/',
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "should-escape-special-char-1",
|
||||
args: args{
|
||||
c: '&',
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "should-escape-special-char-2",
|
||||
args: args{
|
||||
c: '*',
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "should-escape-special-char-3",
|
||||
args: args{
|
||||
c: '(',
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := shouldEscape(tt.args.c); got != tt.want {
|
||||
t.Errorf("shouldEscape() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_escapePath(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty-string",
|
||||
args: args{
|
||||
s: "",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "alphanum-path",
|
||||
args: args{
|
||||
s: "/test-bucket/test-key",
|
||||
},
|
||||
want: "/test-bucket/test-key",
|
||||
},
|
||||
{
|
||||
name: "path-with-unescapable-chars",
|
||||
args: args{
|
||||
s: "/test~bucket/test.key",
|
||||
},
|
||||
want: "/test~bucket/test.key",
|
||||
},
|
||||
{
|
||||
name: "path-with-escapable-chars",
|
||||
args: args{
|
||||
s: "/bucket-*(/test=key&",
|
||||
},
|
||||
want: "/bucket-%2A%28/test%3Dkey%26",
|
||||
},
|
||||
{
|
||||
name: "path-with-space",
|
||||
args: args{
|
||||
s: "/test-bucket/my key",
|
||||
},
|
||||
want: "/test-bucket/my%20key",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := escapePath(tt.args.s); got != tt.want {
|
||||
t.Errorf("escapePath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsChecksumAlgorithmValid(t *testing.T) {
|
||||
type args struct {
|
||||
alg types.ChecksumAlgorithm
|
||||
|
||||
Reference in New Issue
Block a user