Virtual host style S3 requests (#5095)

master
Krishna Srinivas 7 years ago committed by Dee Koder
parent d57d57ddf5
commit e7a724de0d
  1. 2
      cmd/admin-handlers.go
  2. 10
      cmd/api-errors.go
  3. 130
      cmd/api-router.go
  4. 6
      cmd/auth-handler.go
  5. 5
      cmd/bucket-handlers.go
  6. 7
      cmd/common-main.go
  7. 115
      cmd/config-migrate.go
  8. 2
      cmd/config-migrate_test.go
  9. 18
      cmd/config-old.go
  10. 54
      cmd/config-v20.go
  11. 12
      cmd/config-v20_test.go
  12. 132
      cmd/gateway-router.go
  13. 5
      cmd/generic-handlers.go
  14. 3
      cmd/globals.go
  15. 28
      cmd/handler-utils.go
  16. 23
      cmd/handler-utils_test.go
  17. 10
      cmd/signature-v2.go
  18. 14
      docs/config/README.md

@ -985,7 +985,7 @@ func (adminAPI adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http
return return
} }
var config serverConfigV19 var config serverConfigV20
err = json.Unmarshal(configBytes, &config) err = json.Unmarshal(configBytes, &config)
if err != nil { if err != nil {

@ -162,6 +162,7 @@ const (
ErrServerNotInitialized ErrServerNotInitialized
ErrOperationTimedOut ErrOperationTimedOut
ErrPartsSizeUnequal ErrPartsSizeUnequal
ErrInvalidRequest
// Add new extended error codes here. // Add new extended error codes here.
// Please open a https://github.com/minio/minio/issues before adding // Please open a https://github.com/minio/minio/issues before adding
// new error codes here. // new error codes here.
@ -731,6 +732,15 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds", Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
// Generic Invalid-Request error. Should be used for response errors only for unlikely
// corner case errors for which introducing new APIErrorCode is not worth it. errorIf()
// should be used to log the error at the source of the error for debugging purposes.
ErrInvalidRequest: {
Code: "InvalidRequest",
Description: "Invalid Request",
HTTPStatusCode: http.StatusBadRequest,
},
// Add your error structure here. // Add your error structure here.
} }

@ -16,9 +16,8 @@
package cmd package cmd
import ( import router "github.com/gorilla/mux"
router "github.com/gorilla/mux" import "net/http"
)
// objectAPIHandler implements and provides http handlers for S3 API. // objectAPIHandler implements and provides http handlers for S3 API.
type objectAPIHandlers struct { type objectAPIHandlers struct {
@ -34,70 +33,75 @@ func registerAPIRouter(mux *router.Router) {
// API Router // API Router
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter() apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
var routers []*router.Router
if globalDomainName != "" {
routers = append(routers, apiRouter.Host("{bucket:.+}."+globalDomainName).Subrouter())
}
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
// Bucket router for _, bucket := range routers {
bucket := apiRouter.PathPrefix("/{bucket}").Subrouter() // Object operations
// HeadObject
/// Object operations bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.HeadObjectHandler))
// CopyObjectPart
// HeadObject bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.HeadObjectHandler)) // PutObjectPart
// CopyObjectPart bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") // ListObjectPxarts
// PutObjectPart bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.ListObjectPartsHandler)).Queries("uploadId", "{uploadId:.*}")
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") // CompleteMultipartUpload
// ListObjectPxarts bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.CompleteMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.ListObjectPartsHandler)).Queries("uploadId", "{uploadId:.*}") // NewMultipartUpload
// CompleteMultipartUpload bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.NewMultipartUploadHandler)).Queries("uploads", "")
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.CompleteMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}") // AbortMultipartUpload
// NewMultipartUpload bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.NewMultipartUploadHandler)).Queries("uploads", "") // GetObject
// AbortMultipartUpload bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}") // CopyObject
// GetObject bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectHandler))
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler)) // PutObject
// CopyObject bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectHandler))
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectHandler)) // DeleteObject
// PutObject bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.DeleteObjectHandler))
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectHandler))
// DeleteObject
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.DeleteObjectHandler))
/// Bucket operations
// GetBucketLocation /// Bucket operations
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketLocationHandler)).Queries("location", "") // GetBucketLocation
// GetBucketPolicy bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketLocationHandler)).Queries("location", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "") // GetBucketPolicy
// GetBucketNotification bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketNotificationHandler)).Queries("notification", "") // GetBucketNotification
// ListenBucketNotification bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketNotificationHandler)).Queries("notification", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListenBucketNotificationHandler)).Queries("events", "{events:.*}") // ListenBucketNotification
// ListMultipartUploads bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListenBucketNotificationHandler)).Queries("events", "{events:.*}")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListMultipartUploadsHandler)).Queries("uploads", "") // ListMultipartUploads
// ListObjectsV2 bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListMultipartUploadsHandler)).Queries("uploads", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV2Handler)).Queries("list-type", "2") // ListObjectsV2
// ListObjectsV1 (Legacy) bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV2Handler)).Queries("list-type", "2")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV1Handler)) // ListObjectsV1 (Legacy)
// PutBucketPolicy bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV1Handler))
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "") // PutBucketPolicy
// PutBucketNotification bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketNotificationHandler)).Queries("notification", "") // PutBucketNotification
// PutBucket bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketNotificationHandler)).Queries("notification", "")
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketHandler)) // PutBucket
// HeadBucket bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketHandler))
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler)) // HeadBucket
// PostPolicy bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler)) // PostPolicy
// DeleteMultipleObjects bucket.Methods("POST").Path("/").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler))
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)) // DeleteMultipleObjects
// DeleteBucketPolicy bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "") // DeleteBucketPolicy
// DeleteBucket bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketHandler)) // DeleteBucket
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketHandler))
}
/// Root operation /// Root operation
// ListBuckets // ListBuckets
apiRouter.Methods("GET").HandlerFunc(httpTraceAll(api.ListBucketsHandler)) apiRouter.Methods("GET").Path("/").HandlerFunc(httpTraceAll(api.ListBucketsHandler))
// If none of the routes match.
apiRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(notFoundHandler))
} }

@ -127,7 +127,11 @@ func checkRequestAuthType(r *http.Request, bucket, policyAction, region string)
if reqAuthType == authTypeAnonymous && policyAction != "" { if reqAuthType == authTypeAnonymous && policyAction != "" {
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html // http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
sourceIP := getSourceIPAddress(r) sourceIP := getSourceIPAddress(r)
return enforceBucketPolicy(bucket, policyAction, r.URL.Path, resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
if err != nil {
return ErrInternalError
}
return enforceBucketPolicy(bucket, policyAction, resource,
r.Referer(), sourceIP, r.URL.Query()) r.Referer(), sourceIP, r.URL.Query())
} }

@ -24,7 +24,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"path/filepath"
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
@ -441,10 +440,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
// Make sure that the URL does not contain object name. // Make sure that the URL does not contain object name.
bucket := mux.Vars(r)["bucket"] bucket := mux.Vars(r)["bucket"]
if bucket != filepath.Clean(r.URL.Path[1:]) {
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
return
}
// Require Content-Length to be set in the request // Require Content-Length to be set in the request
size := r.ContentLength size := r.ContentLength

@ -57,7 +57,7 @@ func initConfig() {
// Config file does not exist, we create it fresh and return upon success. // Config file does not exist, we create it fresh and return upon success.
if isFile(getConfigFile()) { if isFile(getConfigFile()) {
fatalIf(migrateConfig(), "Config migration failed.") fatalIf(migrateConfig(), "Config migration failed.")
fatalIf(loadConfig(), "Unable to load config version: '%s'.", v19) fatalIf(loadConfig(), "Unable to load config version: '%s'.", v20)
} else { } else {
fatalIf(newConfig(), "Unable to initialize minio config for the first time.") fatalIf(newConfig(), "Unable to initialize minio config for the first time.")
log.Println("Created minio configuration file successfully at " + getConfigDir()) log.Println("Created minio configuration file successfully at " + getConfigDir())
@ -117,4 +117,9 @@ func handleCommonEnvVars() {
} }
globalHTTPTrace = os.Getenv("MINIO_HTTP_TRACE") != "" globalHTTPTrace = os.Getenv("MINIO_HTTP_TRACE") != ""
globalDomainName = os.Getenv("MINIO_DOMAIN")
if globalDomainName != "" {
globalIsEnvDomainName = true
}
} }

@ -148,7 +148,12 @@ func migrateConfig() error {
return err return err
} }
fallthrough fallthrough
case v19: case "19":
if err = migrateV19ToV20(); err != nil {
return err
}
fallthrough
case "20":
// No migration needed. this always points to current version. // No migration needed. this always points to current version.
err = nil err = nil
} }
@ -1479,3 +1484,111 @@ func migrateV18ToV19() error {
log.Printf(configMigrateMSGTemplate, configFile, cv18.Version, srvConfig.Version) log.Printf(configMigrateMSGTemplate, configFile, cv18.Version, srvConfig.Version)
return nil return nil
} }
func migrateV19ToV20() error {
configFile := getConfigFile()
cv19 := &serverConfigV19{}
_, err := quick.Load(configFile, cv19)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("Unable to load config version ‘18’. %v", err)
}
if cv19.Version != "19" {
return nil
}
// Copy over fields from V19 into V20 config struct
srvConfig := &serverConfigV20{
Logger: &loggers{},
Notify: &notifier{},
}
srvConfig.Version = "20"
srvConfig.Credential = cv19.Credential
srvConfig.Region = cv19.Region
if srvConfig.Region == "" {
// Region needs to be set for AWS Signature Version 4.
srvConfig.Region = globalMinioDefaultRegion
}
srvConfig.Logger.Console = cv19.Logger.Console
srvConfig.Logger.File = cv19.Logger.File
// check and set notifiers config
if len(cv19.Notify.AMQP) == 0 {
srvConfig.Notify.AMQP = make(map[string]amqpNotify)
srvConfig.Notify.AMQP["1"] = amqpNotify{}
} else {
// New deliveryMode parameter is added for AMQP,
// default value is already 0, so nothing to
// explicitly migrate here.
srvConfig.Notify.AMQP = cv19.Notify.AMQP
}
if len(cv19.Notify.ElasticSearch) == 0 {
srvConfig.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
srvConfig.Notify.ElasticSearch["1"] = elasticSearchNotify{
Format: formatNamespace,
}
} else {
srvConfig.Notify.ElasticSearch = cv19.Notify.ElasticSearch
}
if len(cv19.Notify.Redis) == 0 {
srvConfig.Notify.Redis = make(map[string]redisNotify)
srvConfig.Notify.Redis["1"] = redisNotify{
Format: formatNamespace,
}
} else {
srvConfig.Notify.Redis = cv19.Notify.Redis
}
if len(cv19.Notify.PostgreSQL) == 0 {
srvConfig.Notify.PostgreSQL = make(map[string]postgreSQLNotify)
srvConfig.Notify.PostgreSQL["1"] = postgreSQLNotify{
Format: formatNamespace,
}
} else {
srvConfig.Notify.PostgreSQL = cv19.Notify.PostgreSQL
}
if len(cv19.Notify.Kafka) == 0 {
srvConfig.Notify.Kafka = make(map[string]kafkaNotify)
srvConfig.Notify.Kafka["1"] = kafkaNotify{}
} else {
srvConfig.Notify.Kafka = cv19.Notify.Kafka
}
if len(cv19.Notify.NATS) == 0 {
srvConfig.Notify.NATS = make(map[string]natsNotify)
srvConfig.Notify.NATS["1"] = natsNotify{}
} else {
srvConfig.Notify.NATS = cv19.Notify.NATS
}
if len(cv19.Notify.Webhook) == 0 {
srvConfig.Notify.Webhook = make(map[string]webhookNotify)
srvConfig.Notify.Webhook["1"] = webhookNotify{}
} else {
srvConfig.Notify.Webhook = cv19.Notify.Webhook
}
if len(cv19.Notify.MySQL) == 0 {
srvConfig.Notify.MySQL = make(map[string]mySQLNotify)
srvConfig.Notify.MySQL["1"] = mySQLNotify{
Format: formatNamespace,
}
} else {
srvConfig.Notify.MySQL = cv19.Notify.MySQL
}
if len(cv19.Notify.MQTT) == 0 {
srvConfig.Notify.MQTT = make(map[string]mqttNotify)
srvConfig.Notify.MQTT["1"] = mqttNotify{}
} else {
srvConfig.Notify.MQTT = cv19.Notify.MQTT
}
// Load browser config from existing config in the file.
srvConfig.Browser = cv19.Browser
if err = quick.Save(configFile, srvConfig); err != nil {
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv19.Version, srvConfig.Version, err)
}
log.Printf(configMigrateMSGTemplate, configFile, cv19.Version, srvConfig.Version)
return nil
}

@ -169,7 +169,7 @@ func TestServerConfigMigrateV2toV19(t *testing.T) {
} }
// Check the version number in the upgraded config file // Check the version number in the upgraded config file
expectedVersion := v19 expectedVersion := v20
if serverConfig.Version != expectedVersion { if serverConfig.Version != expectedVersion {
t.Fatalf("Expect version "+expectedVersion+", found: %v", serverConfig.Version) t.Fatalf("Expect version "+expectedVersion+", found: %v", serverConfig.Version)
} }

@ -472,3 +472,21 @@ type serverConfigV18 struct {
// Notification queue configuration. // Notification queue configuration.
Notify *notifier `json:"notify"` Notify *notifier `json:"notify"`
} }
// serverConfigV19 server configuration version '19' which is like
// version '18' except it adds support for MQTT notifications.
type serverConfigV19 struct {
sync.RWMutex
Version string `json:"version"`
// S3 API configuration.
Credential auth.Credentials `json:"credential"`
Region string `json:"region"`
Browser BrowserFlag `json:"browser"`
// Additional error logging configuration.
Logger *loggers `json:"logger"`
// Notification queue configuration.
Notify *notifier `json:"notify"`
}

@ -28,17 +28,17 @@ import (
) )
// Config version // Config version
const v19 = "19" const v20 = "20"
var ( var (
// serverConfig server config. // serverConfig server config.
serverConfig *serverConfigV19 serverConfig *serverConfigV20
serverConfigMu sync.RWMutex serverConfigMu sync.RWMutex
) )
// serverConfigV19 server configuration version '19' which is like // serverConfigV20 server configuration version '20' which is like
// version '18' except it adds support for MQTT notifications. // version '19' except it adds support for VirtualHostDomain
type serverConfigV19 struct { type serverConfigV20 struct {
sync.RWMutex sync.RWMutex
Version string `json:"version"` Version string `json:"version"`
@ -46,6 +46,7 @@ type serverConfigV19 struct {
Credential auth.Credentials `json:"credential"` Credential auth.Credentials `json:"credential"`
Region string `json:"region"` Region string `json:"region"`
Browser BrowserFlag `json:"browser"` Browser BrowserFlag `json:"browser"`
Domain string `json:"domain"`
// Additional error logging configuration. // Additional error logging configuration.
Logger *loggers `json:"logger"` Logger *loggers `json:"logger"`
@ -55,7 +56,7 @@ type serverConfigV19 struct {
} }
// GetVersion get current config version. // GetVersion get current config version.
func (s *serverConfigV19) GetVersion() string { func (s *serverConfigV20) GetVersion() string {
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
@ -63,7 +64,7 @@ func (s *serverConfigV19) GetVersion() string {
} }
// SetRegion set a new region. // SetRegion set a new region.
func (s *serverConfigV19) SetRegion(region string) { func (s *serverConfigV20) SetRegion(region string) {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
@ -72,7 +73,7 @@ func (s *serverConfigV19) SetRegion(region string) {
} }
// GetRegion get current region. // GetRegion get current region.
func (s *serverConfigV19) GetRegion() string { func (s *serverConfigV20) GetRegion() string {
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
@ -80,7 +81,7 @@ func (s *serverConfigV19) GetRegion() string {
} }
// SetCredentials set new credentials. SetCredential returns the previous credential. // SetCredentials set new credentials. SetCredential returns the previous credential.
func (s *serverConfigV19) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) { func (s *serverConfigV20) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
@ -95,7 +96,7 @@ func (s *serverConfigV19) SetCredential(creds auth.Credentials) (prevCred auth.C
} }
// GetCredentials get current credentials. // GetCredentials get current credentials.
func (s *serverConfigV19) GetCredential() auth.Credentials { func (s *serverConfigV20) GetCredential() auth.Credentials {
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
@ -103,7 +104,7 @@ func (s *serverConfigV19) GetCredential() auth.Credentials {
} }
// SetBrowser set if browser is enabled. // SetBrowser set if browser is enabled.
func (s *serverConfigV19) SetBrowser(b bool) { func (s *serverConfigV20) SetBrowser(b bool) {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
@ -112,7 +113,7 @@ func (s *serverConfigV19) SetBrowser(b bool) {
} }
// GetCredentials get current credentials. // GetCredentials get current credentials.
func (s *serverConfigV19) GetBrowser() bool { func (s *serverConfigV20) GetBrowser() bool {
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
@ -120,7 +121,7 @@ func (s *serverConfigV19) GetBrowser() bool {
} }
// Save config. // Save config.
func (s *serverConfigV19) Save() error { func (s *serverConfigV20) Save() error {
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
@ -128,9 +129,9 @@ func (s *serverConfigV19) Save() error {
return quick.Save(getConfigFile(), s) return quick.Save(getConfigFile(), s)
} }
func newServerConfigV19() *serverConfigV19 { func newServerConfigV20() *serverConfigV20 {
srvCfg := &serverConfigV19{ srvCfg := &serverConfigV20{
Version: v19, Version: v20,
Credential: auth.MustGetNewCredentials(), Credential: auth.MustGetNewCredentials(),
Region: globalMinioDefaultRegion, Region: globalMinioDefaultRegion,
Browser: true, Browser: true,
@ -168,7 +169,7 @@ func newServerConfigV19() *serverConfigV19 {
// found, otherwise use default parameters // found, otherwise use default parameters
func newConfig() error { func newConfig() error {
// Initialize server config. // Initialize server config.
srvCfg := newServerConfigV19() srvCfg := newServerConfigV20()
// If env is set override the credentials from config file. // If env is set override the credentials from config file.
if globalIsEnvCreds { if globalIsEnvCreds {
@ -183,6 +184,10 @@ func newConfig() error {
srvCfg.SetRegion(globalServerRegion) srvCfg.SetRegion(globalServerRegion)
} }
if globalIsEnvDomainName {
srvCfg.Domain = globalDomainName
}
// hold the mutex lock before a new config is assigned. // hold the mutex lock before a new config is assigned.
// Save the new config globally. // Save the new config globally.
// unlock the mutex. // unlock the mutex.
@ -246,8 +251,8 @@ func checkDupJSONKeys(json string) error {
} }
// getValidConfig - returns valid server configuration // getValidConfig - returns valid server configuration
func getValidConfig() (*serverConfigV19, error) { func getValidConfig() (*serverConfigV20, error) {
srvCfg := &serverConfigV19{ srvCfg := &serverConfigV20{
Region: globalMinioDefaultRegion, Region: globalMinioDefaultRegion,
Browser: true, Browser: true,
} }
@ -257,8 +262,8 @@ func getValidConfig() (*serverConfigV19, error) {
return nil, err return nil, err
} }
if srvCfg.Version != v19 { if srvCfg.Version != v20 {
return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", v19, srvCfg.Version) return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", v20, srvCfg.Version)
} }
// Load config file json and check for duplication json keys // Load config file json and check for duplication json keys
@ -312,6 +317,10 @@ func loadConfig() error {
srvCfg.SetRegion(globalServerRegion) srvCfg.SetRegion(globalServerRegion)
} }
if globalIsEnvDomainName {
srvCfg.Domain = globalDomainName
}
// hold the mutex lock before a new config is assigned. // hold the mutex lock before a new config is assigned.
serverConfigMu.Lock() serverConfigMu.Lock()
serverConfig = srvCfg serverConfig = srvCfg
@ -324,6 +333,9 @@ func loadConfig() error {
if !globalIsEnvRegion { if !globalIsEnvRegion {
globalServerRegion = serverConfig.GetRegion() globalServerRegion = serverConfig.GetRegion()
} }
if !globalIsEnvDomainName {
globalDomainName = serverConfig.Domain
}
serverConfigMu.Unlock() serverConfigMu.Unlock()
return nil return nil

@ -117,8 +117,8 @@ func TestServerConfig(t *testing.T) {
serverConfig.Logger.SetFile(fileLogger) serverConfig.Logger.SetFile(fileLogger)
// Match version. // Match version.
if serverConfig.GetVersion() != v19 { if serverConfig.GetVersion() != v20 {
t.Errorf("Expecting version %s found %s", serverConfig.GetVersion(), v19) t.Errorf("Expecting version %s found %s", serverConfig.GetVersion(), v20)
} }
// Attempt to save. // Attempt to save.
@ -149,6 +149,9 @@ func TestServerConfigWithEnvs(t *testing.T) {
os.Setenv("MINIO_REGION", "us-west-1") os.Setenv("MINIO_REGION", "us-west-1")
defer os.Unsetenv("MINIO_REGION") defer os.Unsetenv("MINIO_REGION")
os.Setenv("MINIO_DOMAIN", "domain.com")
defer os.Unsetenv("MINIO_DOMAIN")
defer resetGlobalIsEnvs() defer resetGlobalIsEnvs()
// Get test root. // Get test root.
@ -189,6 +192,9 @@ func TestServerConfigWithEnvs(t *testing.T) {
t.Errorf("Expecting access key to be `minio123` found %s", cred.SecretKey) t.Errorf("Expecting access key to be `minio123` found %s", cred.SecretKey)
} }
if serverConfig.Domain != "domain.com" {
t.Errorf("Expecting Domain to be `domain.com` found " + serverConfig.Domain)
}
} }
func TestCheckDupJSONKeys(t *testing.T) { func TestCheckDupJSONKeys(t *testing.T) {
@ -231,7 +237,7 @@ func TestValidateConfig(t *testing.T) {
configPath := filepath.Join(rootPath, minioConfigFile) configPath := filepath.Join(rootPath, minioConfigFile)
v := v19 v := v20
testCases := []struct { testCases := []struct {
configData string configData string

@ -18,6 +18,7 @@ package cmd
import ( import (
"io" "io"
"net/http"
router "github.com/gorilla/mux" router "github.com/gorilla/mux"
"github.com/minio/minio-go/pkg/policy" "github.com/minio/minio-go/pkg/policy"
@ -62,69 +63,78 @@ func registerGatewayAPIRouter(mux *router.Router, gw GatewayLayer) {
// API Router // API Router
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter() apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
// Bucket router var routers []*router.Router
bucket := apiRouter.PathPrefix("/{bucket}").Subrouter() if globalDomainName != "" {
routers = append(routers, apiRouter.Host("{bucket:.+}."+globalDomainName).Subrouter())
/// Object operations }
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
// HeadObject
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.HeadObjectHandler)) // Object operations
// CopyObjectPart for _, bucket := range routers {
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") /// Object operations
// PutObjectPart
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}") // HeadObject
// ListObjectPxarts bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.HeadObjectHandler))
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.ListObjectPartsHandler)).Queries("uploadId", "{uploadId:.*}") // CopyObjectPart
// CompleteMultipartUpload bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.CompleteMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}") // PutObjectPart
// NewMultipartUpload bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.NewMultipartUploadHandler)).Queries("uploads", "") // ListObjectPxarts
// AbortMultipartUpload bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.ListObjectPartsHandler)).Queries("uploadId", "{uploadId:.*}")
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}") // CompleteMultipartUpload
// GetObject bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.CompleteMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler)) // NewMultipartUpload
// CopyObject bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.NewMultipartUploadHandler)).Queries("uploads", "")
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectHandler)) // AbortMultipartUpload
// PutObject bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectHandler)) // GetObject
// DeleteObject bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.DeleteObjectHandler)) // CopyObject
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectHandler))
/// Bucket operations // PutObject
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectHandler))
// GetBucketLocation // DeleteObject
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketLocationHandler)).Queries("location", "") bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.DeleteObjectHandler))
// GetBucketPolicy
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "") /// Bucket operations
// GetBucketNotification
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketNotificationHandler)).Queries("notification", "") // GetBucketLocation
// ListenBucketNotification bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketLocationHandler)).Queries("location", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListenBucketNotificationHandler)).Queries("events", "{events:.*}") // GetBucketPolicy
// ListMultipartUploads bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListMultipartUploadsHandler)).Queries("uploads", "") // GetBucketNotification
// ListObjectsV2 bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketNotificationHandler)).Queries("notification", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV2Handler)).Queries("list-type", "2") // ListenBucketNotification
// ListObjectsV1 (Legacy) bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListenBucketNotificationHandler)).Queries("events", "{events:.*}")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV1Handler)) // ListMultipartUploads
// PutBucketPolicy bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListMultipartUploadsHandler)).Queries("uploads", "")
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "") // ListObjectsV2
// PutBucketNotification bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV2Handler)).Queries("list-type", "2")
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketNotificationHandler)).Queries("notification", "") // ListObjectsV1 (Legacy)
// PutBucket bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV1Handler))
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketHandler)) // PutBucketPolicy
// HeadBucket bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler)) // PutBucketNotification
// PostPolicy bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketNotificationHandler)).Queries("notification", "")
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler)) // PutBucket
// DeleteMultipleObjects bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketHandler))
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)) // HeadBucket
// DeleteBucketPolicy bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "") // PostPolicy
// DeleteBucket bucket.Methods("POST").Path("/").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler))
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketHandler)) // DeleteMultipleObjects
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
// DeleteBucketPolicy
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "")
// DeleteBucket
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketHandler))
}
/// Root operation /// Root operation
// ListBuckets // ListBuckets
apiRouter.Methods("GET").HandlerFunc(httpTraceAll(api.ListBucketsHandler)) apiRouter.Methods("GET").Path("/").HandlerFunc(httpTraceAll(api.ListBucketsHandler))
// If none of the routes match.
apiRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(notFoundHandler))
} }

@ -440,11 +440,6 @@ func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
// A put method on path "/" doesn't make sense, ignore it.
if r.Method == httpPUT && r.URL.Path == "/" && r.Header.Get(minioAdminOpHeader) == "" {
writeErrorResponse(w, ErrNotImplemented, r.URL)
return
}
// Serve HTTP. // Serve HTTP.
h.handler.ServeHTTP(w, r) h.handler.ServeHTTP(w, r)

@ -146,6 +146,9 @@ var (
globalActiveCred auth.Credentials globalActiveCred auth.Credentials
globalPublicCerts []*x509.Certificate globalPublicCerts []*x509.Certificate
globalXLObjCacheDisabled bool globalXLObjCacheDisabled bool
globalIsEnvDomainName bool
globalDomainName string // Root domain for virtual host style requests
// Add new variable global values here. // Add new variable global values here.
globalListingTimeout = newDynamicTimeout( /*30*/ 600*time.Second /*5*/, 600*time.Second) // timeout for listing related ops globalListingTimeout = newDynamicTimeout( /*30*/ 600*time.Second /*5*/, 600*time.Second) // timeout for listing related ops

@ -19,6 +19,7 @@ package cmd
import ( import (
"io" "io"
"mime/multipart" "mime/multipart"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -259,3 +260,30 @@ func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
} }
return httptracer.TraceReqHandlerFunc(f, os.Stdout, false) return httptracer.TraceReqHandlerFunc(f, os.Stdout, false)
} }
// Returns "/bucketName/objectName" for path-style or virtual-host-style requests.
func getResource(path string, host string, domain string) (string, error) {
if domain == "" {
return path, nil
}
// If virtual-host-style is enabled construct the "resource" properly.
if strings.Contains(host, ":") {
// In bucket.mydomain.com:9000, strip out :9000
var err error
if host, _, err = net.SplitHostPort(host); err != nil {
errorIf(err, "Unable to split %s", host)
return "", err
}
}
if !strings.HasSuffix(host, "."+domain) {
return path, nil
}
bucket := strings.TrimSuffix(host, "."+domain)
return slashSeparator + pathJoin(bucket, path), nil
}
// If none of the http routes match respond with MethodNotAllowed
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
return
}

@ -190,3 +190,26 @@ func TestExtractMetadataHeaders(t *testing.T) {
} }
} }
} }
// Test getResource()
func TestGetResource(t *testing.T) {
testCases := []struct {
p string
host string
domain string
expectedResource string
}{
{"/a/b/c", "test.mydomain.com", "mydomain.com", "/test/a/b/c"},
{"/a/b/c", "test.mydomain.com", "notmydomain.com", "/a/b/c"},
{"/a/b/c", "test.mydomain.com", "", "/a/b/c"},
}
for i, test := range testCases {
gotResource, err := getResource(test.p, test.host, test.domain)
if err != nil {
t.Fatal(err)
}
if gotResource != test.expectedResource {
t.Fatalf("test %d: expected %s got %s", i+1, test.expectedResource, gotResource)
}
}
}

@ -157,6 +157,11 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
return ErrExpiredPresignRequest return ErrExpiredPresignRequest
} }
encodedResource, err = getResource(encodedResource, r.Host, globalDomainName)
if err != nil {
return ErrInvalidRequest
}
expectedSignature := preSignatureV2(r.Method, encodedResource, strings.Join(filteredQueries, "&"), r.Header, expires) expectedSignature := preSignatureV2(r.Method, encodedResource, strings.Join(filteredQueries, "&"), r.Header, expires)
if gotSignature != expectedSignature { if gotSignature != expectedSignature {
return ErrSignatureDoesNotMatch return ErrSignatureDoesNotMatch
@ -237,6 +242,11 @@ func doesSignV2Match(r *http.Request) APIErrorCode {
return ErrInvalidQueryParams return ErrInvalidQueryParams
} }
encodedResource, err = getResource(encodedResource, r.Host, globalDomainName)
if err != nil {
return ErrInvalidRequest
}
expectedAuth := signatureV2(r.Method, encodedResource, strings.Join(unescapedQueries, "&"), r.Header) expectedAuth := signatureV2(r.Method, encodedResource, strings.Join(unescapedQueries, "&"), r.Header)
if v2Auth != expectedAuth { if v2Auth != expectedAuth {
return ErrSignatureDoesNotMatch return ErrSignatureDoesNotMatch

@ -69,6 +69,20 @@ export MINIO_BROWSER=off
minio server /data minio server /data
``` ```
### Domain
|Field|Type|Description|
|:---|:---|:---|
|``domain``| _string_ | Enable virtual-host-style requests i.e http://bucket.mydomain.com/object|
By default, Minio supports path-style requests which look like http://mydomain.com/bucket/object. MINIO_DOMAIN environmental varialble (or `domain` in config.json) can be used to enable virtual-host-style requests. If the request `Host` header matches with `(.+).mydomain.com` then the mattched pattern `$1` is used as bucket and the path is used as object. More information on path-style and virtual-host-style [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html)
Example:
```sh
export MINIO_DOMAIN=mydomain.com
minio server /data
```
#### Logger #### Logger
|Field|Type|Description| |Field|Type|Description|
|:---|:---|:---| |:---|:---|:---|

Loading…
Cancel
Save