From cd0f350c02cbd7821409bdd3e1363310b59bf84a Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Sat, 10 Dec 2016 00:42:22 -0800 Subject: [PATCH] env: Bring back MINIO_BROWSER env. (#3423) Set MINIO_BROWSER=off to disable web browser completely. Fixes #3422 --- cmd/generic-handlers.go | 201 +++++++++++++++++++---------------- cmd/generic-handlers_test.go | 90 ++++++++++++++++ cmd/globals.go | 6 ++ cmd/routers.go | 54 +++++----- cmd/server-main.go | 7 +- 5 files changed, 243 insertions(+), 115 deletions(-) create mode 100644 cmd/generic-handlers_test.go diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index d5e905136..23a10dfa2 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -1,5 +1,5 @@ /* - * Minio Cloud Storage, (C) 2015 Minio, Inc. + * Minio Cloud Storage, (C) 2015, 2016 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,40 +65,64 @@ func (h requestSizeLimitHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques h.handler.ServeHTTP(w, r) } -// Adds redirect rules for incoming requests. -type redirectHandler struct { - handler http.Handler - locationPrefix string -} - // Reserved bucket. const ( reservedBucket = "/minio" ) +// Adds redirect rules for incoming requests. +type redirectHandler struct { + handler http.Handler +} + func setBrowserRedirectHandler(h http.Handler) http.Handler { - return redirectHandler{handler: h, locationPrefix: reservedBucket} + return redirectHandler{handler: h} +} + +// Fetch redirect location if urlPath satisfies certain +// criteria. Some special names are considered to be +// redirectable, this is purely internal function and +// serves only limited purpose on redirect-handler for +// browser requests. +func getRedirectLocation(urlPath string) (rLocation string) { + if urlPath == reservedBucket { + rLocation = reservedBucket + "/" + } + if contains([]string{ + "/", + "/webrpc", + "/login", + "/favicon.ico", + }, urlPath) { + rLocation = reservedBucket + urlPath + } + return rLocation +} + +// guessIsBrowserReq - returns true if the request is browser. +// This implementation just validates user-agent and +// looks for "Mozilla" string. This is no way certifiable +// way to know if the request really came from a browser +// since User-Agent's can be arbitrary. But this is just +// a best effort function. +func guessIsBrowserReq(req *http.Request) bool { + if req == nil { + return false + } + return strings.Contains(req.Header.Get("User-Agent"), "Mozilla") } func (h redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { aType := getRequestAuthType(r) - // Re-direct only for JWT and anonymous requests coming from web-browser. + // Re-direct only for JWT and anonymous requests from browser. if aType == authTypeJWT || aType == authTypeAnonymous { - // Re-direction handled specifically for browsers. - if strings.Contains(r.Header.Get("User-Agent"), "Mozilla") { - switch r.URL.Path { - case "/", "/webrpc", "/login", "/favicon.ico": - // '/' is redirected to 'locationPrefix/' - // '/webrpc' is redirected to 'locationPrefix/webrpc' - // '/login' is redirected to 'locationPrefix/login' - location := h.locationPrefix + r.URL.Path - // Redirect to new location. - http.Redirect(w, r, location, http.StatusTemporaryRedirect) - return - case h.locationPrefix: - // locationPrefix is redirected to 'locationPrefix/' - location := h.locationPrefix + "/" - http.Redirect(w, r, location, http.StatusTemporaryRedirect) + // Re-direction is handled specifically for browser requests. + if guessIsBrowserReq(r) && globalIsBrowserEnabled { + // Fetch the redirect location if any. + redirectLocation := getRedirectLocation(r.URL.Path) + if redirectLocation != "" { + // Employ a temporary re-direct. + http.Redirect(w, r, redirectLocation, http.StatusTemporaryRedirect) return } } @@ -116,10 +140,11 @@ func setBrowserCacheControlHandler(h http.Handler) http.Handler { } func (h cacheControlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" && strings.Contains(r.Header.Get("User-Agent"), "Mozilla") { + if r.Method == "GET" && guessIsBrowserReq(r) && globalIsBrowserEnabled { // For all browser requests set appropriate Cache-Control policies - match, e := regexp.MatchString(reservedBucket+`/([^/]+\.js|favicon.ico)`, r.URL.Path) - if e != nil { + match, err := regexp.Match(reservedBucket+`/([^/]+\.js|favicon.ico)`, []byte(r.URL.Path)) + if err != nil { + errorIf(err, "Unable to match incoming URL %s", r.URL) writeErrorResponse(w, r, ErrInternalError, r.URL.Path) return } @@ -147,13 +172,22 @@ func setPrivateBucketHandler(h http.Handler) http.Handler { func (h minioPrivateBucketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // For all non browser requests, reject access to 'reservedBucket'. - if !strings.Contains(r.Header.Get("User-Agent"), "Mozilla") && path.Clean(r.URL.Path) == reservedBucket { + if !guessIsBrowserReq(r) && path.Clean(r.URL.Path) == reservedBucket { writeErrorResponse(w, r, ErrAllAccessDisabled, r.URL.Path) return } h.handler.ServeHTTP(w, r) } +type timeValidityHandler struct { + handler http.Handler +} + +// setTimeValidityHandler to validate parsable time over http header +func setTimeValidityHandler(h http.Handler) http.Handler { + return timeValidityHandler{h} +} + // Supported Amz date formats. var amzDateFormats = []string{ time.RFC1123, @@ -162,23 +196,23 @@ var amzDateFormats = []string{ // Add new AMZ date formats here. } -// parseAmzDate - parses date string into supported amz date formats. -func parseAmzDate(amzDateStr string) (amzDate time.Time, apiErr APIErrorCode) { - for _, dateFormat := range amzDateFormats { - amzDate, e := time.Parse(dateFormat, amzDateStr) - if e == nil { - return amzDate, ErrNone - } - } - return time.Time{}, ErrMalformedDate -} - // Supported Amz date headers. var amzDateHeaders = []string{ "x-amz-date", "date", } +// parseAmzDate - parses date string into supported amz date formats. +func parseAmzDate(amzDateStr string) (amzDate time.Time, apiErr APIErrorCode) { + for _, dateFormat := range amzDateFormats { + amzDate, err := time.Parse(dateFormat, amzDateStr) + if err == nil { + return amzDate, ErrNone + } + } + return time.Time{}, ErrMalformedDate +} + // parseAmzDateHeader - parses supported amz date headers, in // supported amz date formats. func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) { @@ -192,15 +226,6 @@ func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) { return time.Time{}, ErrMissingDateHeader } -type timeValidityHandler struct { - handler http.Handler -} - -// setTimeValidityHandler to validate parsable time over http header -func setTimeValidityHandler(h http.Handler) http.Handler { - return timeValidityHandler{h} -} - func (h timeValidityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { aType := getRequestAuthType(r) if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned { @@ -247,47 +272,6 @@ func setIgnoreResourcesHandler(h http.Handler) http.Handler { return resourceHandler{h} } -// Resource handler ServeHTTP() wrapper -func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Skip the first element which is usually '/' and split the rest. - splits := strings.SplitN(r.URL.Path[1:], "/", 2) - - // Save bucketName and objectName extracted from url Path. - var bucketName, objectName string - if len(splits) == 1 { - bucketName = splits[0] - } - if len(splits) == 2 { - bucketName = splits[0] - objectName = splits[1] - } - - // If bucketName is present and not objectName check for bucket level resource queries. - if bucketName != "" && objectName == "" { - if ignoreNotImplementedBucketResources(r) { - writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path) - return - } - } - // If bucketName and objectName are present check for its resource queries. - if bucketName != "" && objectName != "" { - if ignoreNotImplementedObjectResources(r) { - writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path) - return - } - } - // A put method on path "/" doesn't make sense, ignore it. - if r.Method == "PUT" && r.URL.Path == "/" { - writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path) - return - } - - // Serve HTTP. - h.handler.ServeHTTP(w, r) -} - -//// helpers - // Checks requests for not implemented Bucket resources func ignoreNotImplementedBucketResources(req *http.Request) bool { for name := range req.URL.Query() { @@ -328,3 +312,42 @@ var notimplementedObjectResourceNames = map[string]bool{ "acl": true, "policy": true, } + +// Resource handler ServeHTTP() wrapper +func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Skip the first element which is usually '/' and split the rest. + splits := strings.SplitN(r.URL.Path[1:], "/", 2) + + // Save bucketName and objectName extracted from url Path. + var bucketName, objectName string + if len(splits) == 1 { + bucketName = splits[0] + } + if len(splits) == 2 { + bucketName = splits[0] + objectName = splits[1] + } + + // If bucketName is present and not objectName check for bucket level resource queries. + if bucketName != "" && objectName == "" { + if ignoreNotImplementedBucketResources(r) { + writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path) + return + } + } + // If bucketName and objectName are present check for its resource queries. + if bucketName != "" && objectName != "" { + if ignoreNotImplementedObjectResources(r) { + writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path) + return + } + } + // A put method on path "/" doesn't make sense, ignore it. + if r.Method == "PUT" && r.URL.Path == "/" { + writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path) + return + } + + // Serve HTTP. + h.handler.ServeHTTP(w, r) +} diff --git a/cmd/generic-handlers_test.go b/cmd/generic-handlers_test.go new file mode 100644 index 000000000..f1645d030 --- /dev/null +++ b/cmd/generic-handlers_test.go @@ -0,0 +1,90 @@ +/* + * Minio Cloud Storage, (C) 2016 Minio, Inc. + * + * 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 cmd + +import ( + "net/http" + "testing" +) + +// Tests getRedirectLocation function for all its criteria. +func TestRedirectLocation(t *testing.T) { + testCases := []struct { + urlPath string + location string + }{ + { + // 1. When urlPath is '/minio' + urlPath: reservedBucket, + location: reservedBucket + "/", + }, + { + // 2. When urlPath is '/' + urlPath: "/", + location: reservedBucket + "/", + }, + { + // 3. When urlPath is '/webrpc' + urlPath: "/webrpc", + location: reservedBucket + "/webrpc", + }, + { + // 4. When urlPath is '/login' + urlPath: "/login", + location: reservedBucket + "/login", + }, + { + // 5. When urlPath is '/favicon.ico' + urlPath: "/favicon.ico", + location: reservedBucket + "/favicon.ico", + }, + { + // 6. When urlPath is '/unknown' + urlPath: "/unknown", + location: "", + }, + } + + // Validate all conditions. + for i, testCase := range testCases { + loc := getRedirectLocation(testCase.urlPath) + if testCase.location != loc { + t.Errorf("Test %d: Unexpected location expected %s, got %s", i+1, testCase.location, loc) + } + } +} + +// Tests browser request guess function. +func TestGuessIsBrowser(t *testing.T) { + if guessIsBrowserReq(nil) { + t.Fatal("Unexpected return for nil request") + } + r := &http.Request{ + Header: http.Header{}, + } + r.Header.Set("User-Agent", "Mozilla") + if !guessIsBrowserReq(r) { + t.Fatal("Test shouldn't fail for a possible browser request.") + } + r = &http.Request{ + Header: http.Header{}, + } + r.Header.Set("User-Agent", "mc") + if guessIsBrowserReq(r) { + t.Fatal("Test shouldn't report as browser for a non browser request.") + } +} diff --git a/cmd/globals.go b/cmd/globals.go index 0223766b6..bc7aa5b36 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -18,6 +18,8 @@ package cmd import ( "crypto/x509" + "os" + "strings" "time" humanize "github.com/dustin/go-humanize" @@ -61,6 +63,10 @@ var ( globalIsDistXL = false // "Is Distributed?" flag. + // This flag is set to 'true' by default, it is set to `false` + // when MINIO_BROWSER env is set to 'off'. + globalIsBrowserEnabled = !strings.EqualFold(os.Getenv("MINIO_BROWSER"), "off") + // Maximum cache size. Defaults to disabled. // Caching is enabled only for RAM size > 8GiB. globalMaxCacheSize = uint64(0) diff --git a/cmd/routers.go b/cmd/routers.go index 18d0f5dcf..6f03c480f 100644 --- a/cmd/routers.go +++ b/cmd/routers.go @@ -75,6 +75,30 @@ func newObjectLayer(storageDisks []StorageAPI) (ObjectLayer, error) { return objAPI, nil } +// Composed function registering routers for only distributed XL setup. +func registerDistXLRouters(mux *router.Router, srvCmdConfig serverCmdConfig) error { + // Register storage rpc router only if its a distributed setup. + err := registerStorageRPCRouters(mux, srvCmdConfig) + if err != nil { + return err + } + + // Register distributed namespace lock. + err = registerDistNSLockRouter(mux, srvCmdConfig) + if err != nil { + return err + } + + // Register S3 peer communication router. + err = registerS3PeerRPCRouter(mux) + if err != nil { + return err + } + + // Register RPC router for web related calls. + return registerBrowserPeerRPCRouter(mux) +} + // configureServer handler returns final handler for the http server. func configureServerHandler(srvCmdConfig serverCmdConfig) (http.Handler, error) { // Initialize router. `SkipClean(true)` stops gorilla/mux from @@ -83,32 +107,14 @@ func configureServerHandler(srvCmdConfig serverCmdConfig) (http.Handler, error) // Initialize distributed NS lock. if globalIsDistXL { - // Register storage rpc router only if its a distributed setup. - err := registerStorageRPCRouters(mux, srvCmdConfig) - if err != nil { + registerDistXLRouters(mux, srvCmdConfig) + } + + // Register web router when its enabled. + if globalIsBrowserEnabled { + if err := registerWebRouter(mux); err != nil { return nil, err } - - // Register distributed namespace lock. - err = registerDistNSLockRouter(mux, srvCmdConfig) - if err != nil { - return nil, err - } - } - - // Register S3 peer communication router. - err := registerS3PeerRPCRouter(mux) - if err != nil { - return nil, err - } - - // Register RPC router for web related calls. - if err = registerBrowserPeerRPCRouter(mux); err != nil { - return nil, err - } - - if err = registerWebRouter(mux); err != nil { - return nil, err } // Add API router. diff --git a/cmd/server-main.go b/cmd/server-main.go index f21496036..bb45b459d 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -55,8 +55,11 @@ FLAGS: {{end}} ENVIRONMENT VARIABLES: ACCESS: - MINIO_ACCESS_KEY: Username or access key of 5 to 20 characters in length. - MINIO_SECRET_KEY: Password or secret key of 8 to 40 characters in length. + MINIO_ACCESS_KEY: Custom username or access key of 5 to 20 characters in length. + MINIO_SECRET_KEY: Custom password or secret key of 8 to 40 characters in length. + + BROWSER: + MINIO_BROWSER: To disable web browser access, set this value to "off". EXAMPLES: 1. Start minio server on "/home/shared" directory.