Add domain and subdomain support for MinioAPI

This change brings in domain and subdomain support

   - ./minio --domain "yourminiodomain.com"

This change brings in a much needed feature by keeping
bucketnames as part of your 'DNS' name.

All your existing applications can be migrated off from s3 to
Minio without little to no modifications.

NOTE: Setting up DNS for your `buckets` is out of scope of this feature
This commit is contained in:
Harshavardhana 2015-02-23 02:11:27 -08:00
parent 2d3b00b831
commit 51e80eaa6d
13 changed files with 236 additions and 148 deletions

View File

@ -41,6 +41,7 @@ func getStorageType(input string) server.StorageType {
func runCmd(c *cli.Context) {
storageTypeStr := c.String("storage-type")
domain := c.String("domain")
apiaddress := c.String("api-address")
webaddress := c.String("web-address")
certFile := c.String("cert")
@ -52,6 +53,7 @@ func runCmd(c *cli.Context) {
storageType := getStorageType(storageTypeStr)
var serverConfigs []server.ServerConfig
apiServerConfig := server.ServerConfig{
Domain: domain,
Address: apiaddress,
Tls: tls,
CertFile: certFile,
@ -61,6 +63,7 @@ func runCmd(c *cli.Context) {
},
}
webUiServerConfig := server.ServerConfig{
Domain: domain,
Address: webaddress,
Tls: false,
CertFile: "",
@ -79,6 +82,11 @@ func main() {
app.Name = "minio"
app.Usage = ""
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "domain,d",
Value: "",
Usage: "address for incoming API requests",
},
cli.StringFlag{
Name: "api-address,a",
Value: ":9000",

View File

@ -40,7 +40,7 @@ var _ = Suite(&MySuite{})
func (s *MySuite) TestNonExistantObject(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -52,7 +52,7 @@ func (s *MySuite) TestNonExistantObject(c *C) {
func (s *MySuite) TestEmptyObject(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -77,7 +77,7 @@ func (s *MySuite) TestEmptyObject(c *C) {
func (s *MySuite) TestObject(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -100,7 +100,7 @@ func (s *MySuite) TestObject(c *C) {
func (s *MySuite) TestMultipleObjects(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -180,7 +180,7 @@ func (s *MySuite) TestMultipleObjects(c *C) {
func (s *MySuite) TestNotImplemented(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -191,7 +191,7 @@ func (s *MySuite) TestNotImplemented(c *C) {
func (s *MySuite) TestHeader(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -214,7 +214,7 @@ func (s *MySuite) TestHeader(c *C) {
func (s *MySuite) TestPutBucket(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -239,7 +239,7 @@ func (s *MySuite) TestPutBucket(c *C) {
func (s *MySuite) TestPutObject(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -289,7 +289,7 @@ func (s *MySuite) TestPutObject(c *C) {
func (s *MySuite) TestListBuckets(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -368,7 +368,7 @@ func verifyHeaders(c *C, header http.Header, date time.Time, size int, contentTy
func (s *MySuite) TestXMLNameNotInBucketListJson(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -393,7 +393,7 @@ func (s *MySuite) TestXMLNameNotInBucketListJson(c *C) {
func (s *MySuite) TestXMLNameNotInObjectListJson(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()
@ -418,7 +418,7 @@ func (s *MySuite) TestXMLNameNotInObjectListJson(c *C) {
func (s *MySuite) TestContentTypePersists(c *C) {
_, _, storage := inmemory.Start()
httpHandler := HttpHandler(storage)
httpHandler := HttpHandler("", storage)
testServer := httptest.NewServer(httpHandler)
defer testServer.Close()

View File

@ -1,116 +1,13 @@
package minioapi
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
mstorage "github.com/minio-io/minio/pkg/storage"
"github.com/minio-io/minio/pkg/utils/policy"
)
func (server *minioApi) putBucketPolicyHandler(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
bucket := vars["bucket"]
acceptsContentType := getContentType(req)
policy, ok := policy.Parsepolicy(req.Body)
if ok == false {
error := errorCodeError(InvalidPolicyDocument)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
return
}
err := server.storage.StoreBucketPolicy(bucket, policy)
switch err := err.(type) {
case nil:
{
w.WriteHeader(http.StatusNoContent)
writeCommonHeaders(w, getContentString(acceptsContentType))
w.Header().Set("Connection", "keep-alive")
}
case mstorage.BucketNameInvalid:
{
error := errorCodeError(InvalidBucketName)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BucketNotFound:
{
error := errorCodeError(NoSuchBucket)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BackendCorrupted:
case mstorage.ImplementationError:
{
log.Println(err)
error := errorCodeError(InternalError)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
}
}
func (server *minioApi) getBucketPolicyHandler(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
bucket := vars["bucket"]
acceptsContentType := getContentType(req)
p, err := server.storage.GetBucketPolicy(bucket)
switch err := err.(type) {
case nil:
{
responsePolicy, ret := json.Marshal(p)
if ret != nil {
error := errorCodeError(InternalError)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
writeCommonHeaders(w, getContentString(acceptsContentType))
w.Header().Set("Connection", "keep-alive")
w.Write(responsePolicy)
}
case mstorage.BucketNameInvalid:
{
error := errorCodeError(InvalidBucketName)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BucketNotFound:
{
error := errorCodeError(NoSuchBucket)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BucketPolicyNotFound:
{
error := errorCodeError(NoSuchBucketPolicy)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BackendCorrupted:
case mstorage.ImplementationError:
{
log.Println(err)
error := errorCodeError(InternalError)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
}
}
func (server *minioApi) listObjectsHandler(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
bucket := vars["bucket"]
@ -224,7 +121,7 @@ func (server *minioApi) putBucketHandler(w http.ResponseWriter, req *http.Reques
}
case mstorage.ImplementationError:
{
// Embed errors log on serve side
// Embed errors log on server side
log.Println(err)
error := errorCodeError(InternalError)
errorResponse := getErrorResponse(error, bucket)

View File

@ -27,6 +27,11 @@ import (
mstorage "github.com/minio-io/minio/pkg/storage"
)
// No encoder interface exists, so we create one.
type encoder interface {
Encode(v interface{}) error
}
// Write Common Header helpers
func writeCommonHeaders(w http.ResponseWriter, acceptsType string) {
w.Header().Set("Server", "Minio")

View File

@ -20,7 +20,7 @@ import (
"log"
"net/http"
"github.com/gorilla/mux"
x "github.com/gorilla/mux"
mstorage "github.com/minio-io/minio/pkg/storage"
"github.com/minio-io/minio/pkg/utils/config"
)
@ -30,24 +30,11 @@ const (
)
type minioApi struct {
domain string
storage mstorage.Storage
}
// No encoder interface exists, so we create one.
type encoder interface {
Encode(v interface{}) error
}
func HttpHandler(storage mstorage.Storage) http.Handler {
mux := mux.NewRouter()
var api = minioApi{}
api.storage = storage
var conf = config.Config{}
if err := conf.SetupConfig(); err != nil {
log.Fatal(err)
}
func pathMux(api minioApi, mux *x.Router) *x.Router {
mux.HandleFunc("/", api.listBucketsHandler).Methods("GET")
mux.HandleFunc("/{bucket}", api.listObjectsHandler).Methods("GET")
mux.HandleFunc("/{bucket}", api.putBucketHandler).Methods("PUT")
@ -55,5 +42,48 @@ func HttpHandler(storage mstorage.Storage) http.Handler {
mux.HandleFunc("/{bucket}/{object:.*}", api.headObjectHandler).Methods("HEAD")
mux.HandleFunc("/{bucket}/{object:.*}", api.putObjectHandler).Methods("PUT")
return mux
}
func domainMux(api minioApi, mux *x.Router) *x.Router {
mux.HandleFunc("/",
api.listObjectsHandler).Host("{bucket}" + "." + api.domain).Methods("GET")
mux.HandleFunc("/{object:.*}",
api.getObjectHandler).Host("{bucket}" + "." + api.domain).Methods("GET")
mux.HandleFunc("/{object:.*}",
api.headObjectHandler).Host("{bucket}" + "." + api.domain).Methods("HEAD")
mux.HandleFunc("/{object:.*}",
api.putObjectHandler).Host("{bucket}" + "." + api.domain).Methods("PUT")
mux.HandleFunc("/", api.listBucketsHandler).Methods("GET")
mux.HandleFunc("/{bucket}", api.putBucketHandler).Methods("PUT")
return mux
}
func getMux(api minioApi, mux *x.Router) *x.Router {
switch true {
case api.domain == "":
return pathMux(api, mux)
case api.domain != "":
s := mux.Host(api.domain).Subrouter()
return domainMux(api, s)
}
return nil
}
func HttpHandler(domain string, storage mstorage.Storage) http.Handler {
var mux *x.Router
var api = minioApi{}
api.storage = storage
api.domain = domain
r := x.NewRouter()
mux = getMux(api, r)
var conf = config.Config{}
if err := conf.SetupConfig(); err != nil {
log.Fatal(err)
}
return validateHandler(conf, ignoreResourcesHandler(mux))
}

View File

@ -103,6 +103,12 @@ func (server *minioApi) putObjectHandler(w http.ResponseWriter, req *http.Reques
bucket = vars["bucket"]
object = vars["object"]
resources := getBucketResources(req.URL.Query())
if resources.policy == true && object == "" {
server.putBucketPolicyHandler(w, req)
return
}
err := server.storage.StoreObject(bucket, object, "", req.Body)
switch err := err.(type) {
case nil:

View File

@ -0,0 +1,112 @@
package minioapi
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
mstorage "github.com/minio-io/minio/pkg/storage"
"github.com/minio-io/minio/pkg/utils/policy"
)
func (server *minioApi) putBucketPolicyHandler(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
bucket := vars["bucket"]
acceptsContentType := getContentType(req)
policy, ok := policy.Parsepolicy(req.Body)
if ok == false {
error := errorCodeError(InvalidPolicyDocument)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
return
}
err := server.storage.StoreBucketPolicy(bucket, policy)
switch err := err.(type) {
case nil:
{
w.WriteHeader(http.StatusNoContent)
writeCommonHeaders(w, getContentString(acceptsContentType))
w.Header().Set("Connection", "keep-alive")
}
case mstorage.BucketNameInvalid:
{
error := errorCodeError(InvalidBucketName)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BucketNotFound:
{
error := errorCodeError(NoSuchBucket)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BackendCorrupted:
case mstorage.ImplementationError:
{
log.Println(err)
error := errorCodeError(InternalError)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
}
}
func (server *minioApi) getBucketPolicyHandler(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
bucket := vars["bucket"]
acceptsContentType := getContentType(req)
p, err := server.storage.GetBucketPolicy(bucket)
switch err := err.(type) {
case nil:
{
responsePolicy, ret := json.Marshal(p)
if ret != nil {
error := errorCodeError(InternalError)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
writeCommonHeaders(w, getContentString(acceptsContentType))
w.Header().Set("Connection", "keep-alive")
w.Write(responsePolicy)
}
case mstorage.BucketNameInvalid:
{
error := errorCodeError(InvalidBucketName)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BucketNotFound:
{
error := errorCodeError(NoSuchBucket)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BucketPolicyNotFound:
{
error := errorCodeError(NoSuchBucketPolicy)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
case mstorage.BackendCorrupted:
case mstorage.ImplementationError:
{
log.Println(err)
error := errorCodeError(InternalError)
errorResponse := getErrorResponse(error, bucket)
w.WriteHeader(error.HttpStatusCode)
w.Write(writeErrorResponse(w, errorResponse, acceptsContentType))
}
}
}

View File

@ -27,7 +27,7 @@ type HttpServerConfig struct {
TLS bool
CertFile string
KeyFile string
Websocket bool
Websocket bool // implement it - TODO
}
type HttpServer struct{}

View File

@ -31,6 +31,7 @@ import (
)
type ServerConfig struct {
Domain string
Address string
Tls bool
CertFile string
@ -78,7 +79,7 @@ func getHttpChannels(configs []ServerConfig) (ctrlChans []chan<- string, statusC
ctrlChans, statusChans, storage = getStorageChannels(k.StorageType)
// start minio api in a web server, pass storage driver into it
ctrlChan, statusChan, _ = httpserver.Start(minioapi.HttpHandler(storage), httpConfig)
ctrlChan, statusChan, _ = httpserver.Start(minioapi.HttpHandler(config.Domain, storage), httpConfig)
ctrlChans = append(ctrlChans, ctrlChan)
statusChans = append(statusChans, statusChan)

View File

@ -127,14 +127,15 @@ func (storage *storage) GetBucketPolicy(bucket string) (interface{}, error) {
// get policy path
bucketPolicy := path.Join(storage.root, bucket+"_policy.json")
filestat, err := os.Stat(bucketPolicy)
if filestat.IsDir() {
return policy.BucketPolicy{}, mstorage.BackendCorrupted{Path: bucketPolicy}
}
if os.IsNotExist(err) {
return policy.BucketPolicy{}, mstorage.BucketPolicyNotFound{Bucket: bucket}
}
if filestat.IsDir() {
return policy.BucketPolicy{}, mstorage.BackendCorrupted{Path: bucketPolicy}
}
file, err := os.OpenFile(bucketPolicy, os.O_RDONLY, 0666)
defer file.Close()
if err != nil {
@ -170,10 +171,13 @@ func (storage *storage) StoreBucketPolicy(bucket string, policy interface{}) err
// get policy path
bucketPolicy := path.Join(storage.root, bucket+"_policy.json")
filestat, _ := os.Stat(bucketPolicy)
filestat, ret := os.Stat(bucketPolicy)
if !os.IsNotExist(ret) {
if filestat.IsDir() {
return mstorage.BackendCorrupted{Path: bucketPolicy}
}
}
file, err := os.OpenFile(bucketPolicy, os.O_WRONLY|os.O_CREATE, 0600)
defer file.Close()
if err != nil {

View File

@ -61,7 +61,8 @@ func IsValidBucket(bucket string) bool {
if match, _ := regexp.MatchString("\\.\\.", bucket); match == true {
return false
}
match, _ := regexp.MatchString("^[a-zA-Z][a-zA-Z0-9\\.\\-]+[a-zA-Z0-9]$", bucket)
// We don't support buckets with '.' in them
match, _ := regexp.MatchString("^[a-zA-Z][a-zA-Z0-9\\-]+[a-zA-Z0-9]$", bucket)
return match
}

View File

@ -67,9 +67,9 @@ func ValidateRequest(user config.User, req *http.Request) (bool, error) {
encoder.Close()
// DEBUG
//fmt.Println("Request header sent: ", req.Header.Get("Authorization"))
//fmt.Println("Header calculated: ", authHeader.String())
//fmt.Printf("%q : %x", ss, ss)
// fmt.Println("Request header sent: ", req.Header.Get("Authorization"))
// fmt.Println("Header calculated: ", authHeader.String())
// fmt.Printf("%q : %x", ss, ss)
if req.Header.Get("Authorization") != authHeader.String() {
return false, fmt.Errorf("Authorization header mismatch")
}
@ -155,6 +155,11 @@ var subResList = []string{"acl", "lifecycle", "location", "logging", "notificati
// <HTTP-Request-URI, from the protocol name up to the query string> +
// [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
func writeCanonicalizedResource(buf *bytes.Buffer, req *http.Request) {
bucket := bucketFromHostname(req)
if bucket != "" {
buf.WriteByte('/')
buf.WriteString(bucket)
}
buf.WriteString(req.URL.Path)
if req.URL.RawQuery != "" {
n := 0
@ -176,3 +181,17 @@ func writeCanonicalizedResource(buf *bytes.Buffer, req *http.Request) {
}
}
}
func bucketFromHostname(req *http.Request) string {
host := req.Host
if host == "" {
host = req.URL.Host
}
host = strings.TrimSpace(host)
hostParts := strings.Split(host, ".")
if len(hostParts) > 1 {
return hostParts[0]
}
return ""
}

View File

@ -31,8 +31,8 @@ const (
// TODO support canonical user
const (
AwsPrincipal = "arn:aws:iam::Account-ID:user/"
MinioPrincipal = "minio::Account-ID:user/"
AwsPrincipal = "arn:aws:iam::"
MinioPrincipal = "minio::"
)
var SupportedActionMap = map[string]bool{
@ -55,10 +55,13 @@ var SupportedEffectMap = map[string]bool{
func isValidAction(action []string) bool {
var ok bool = false
for _, a := range action {
if SupportedActionMap[a] {
if !SupportedActionMap[a] {
goto error
}
}
ok = true
}
}
error:
return ok
}
@ -104,6 +107,7 @@ func isValidPrincipal(principal string) bool {
if len(username) == 0 {
ok = false
}
case strings.HasPrefix(principal, MinioPrincipal):
username := strings.SplitAfter(principal, MinioPrincipal)[1]
ok = true
@ -160,6 +164,7 @@ func Parsepolicy(data io.Reader) (BucketPolicy, bool) {
if len(statement.Resource) == 0 {
goto error
}
if !isValidResource(statement.Resource) {
goto error
}