-
Notifications
You must be signed in to change notification settings - Fork 0
feat: redirect anonymous GET / to the panel #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "net" | ||
| "net/http" | ||
| "slices" | ||
| "strings" | ||
| ) | ||
|
|
||
| // RootToPanelRedirect wraps the S3 handler so that an anonymous GET to the | ||
| // server root ("/") is redirected to the panel, while all genuine S3 requests | ||
| // are passed through untouched. | ||
| // | ||
| // A root-level GET is the S3 ListBuckets operation, which requires SigV4 | ||
| // authentication (it returns AccessDenied for anonymous requests). Browsers | ||
| // never sign requests, so an unauthenticated GET / can only be a user | ||
| // navigating to the server URL. Real S3 clients always supply an auth marker, | ||
| // so none of them are affected by the redirect. | ||
| func RootToPanelRedirect(next http.Handler, hostBases []string) http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method == http.MethodGet && r.URL.Path == "/" && !isS3RootRequest(r, hostBases) { | ||
| http.Redirect(w, r, "/_panel/", http.StatusFound) | ||
| return | ||
| } | ||
| next.ServeHTTP(w, r) | ||
| }) | ||
| } | ||
|
|
||
| // isS3RootRequest reports whether a bare GET / should be treated as an S3 API | ||
| // request rather than a browser navigation: | ||
| // - authenticated requests (SigV4 Authorization header) | ||
| // - presigned requests (X-Amz-* query parameters) | ||
| // - virtual-host style requests addressed to a bucket subdomain | ||
| // ({bucket}.{hostBase}), which serve bucket object listings at "/". | ||
| func isS3RootRequest(r *http.Request, hostBases []string) bool { | ||
| if r.Header.Get("Authorization") != "" { | ||
| return true | ||
| } | ||
| for key := range r.URL.Query() { | ||
| if strings.HasPrefix(key, "X-Amz-") { | ||
| return true | ||
| } | ||
| } | ||
| return hostMatchesBucketBase(r.Host, hostBases) | ||
| } | ||
|
|
||
| // hostMatchesBucketBase reports whether host addresses a bucket as a subdomain | ||
| // of one of the effective host bucket bases, mirroring the bucket-from-host | ||
| // logic used by the S3 handler. Like s3d, "localhost" is always treated as a | ||
| // base so virtual-host-style requests work out of the box during local | ||
| // development. | ||
| func hostMatchesBucketBase(host string, hostBases []string) bool { | ||
| if h, _, err := net.SplitHostPort(host); err == nil { | ||
| host = h | ||
| } | ||
| if !slices.Contains(hostBases, "localhost") { | ||
| hostBases = append(hostBases, "localhost") | ||
| } | ||
| for _, base := range hostBases { | ||
| suffix := "." + strings.Trim(base, ".") | ||
| if !strings.HasSuffix(host, suffix) { | ||
| continue | ||
| } | ||
| bucket := host[:len(host)-len(suffix)] | ||
| if bucket != "" && !strings.Contains(bucket, ".") { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestRootToPanelRedirect(t *testing.T) { | ||
| hostBases := []string{"s3.example.com"} | ||
|
|
||
| // next records whether it was reached (i.e. request passed through to S3). | ||
| next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte("s3")) | ||
| }) | ||
| h := RootToPanelRedirect(next, hostBases) | ||
|
|
||
| run := func(method, target, host string) *httptest.ResponseRecorder { | ||
| req := httptest.NewRequest(method, target, nil) | ||
| req.Host = host | ||
| rec := httptest.NewRecorder() | ||
| h.ServeHTTP(rec, req) | ||
| return rec | ||
| } | ||
|
|
||
| t.Run("anonymous root GET redirects to panel", func(t *testing.T) { | ||
| rec := run(http.MethodGet, "/", "example.com") | ||
| require.Equal(t, http.StatusFound, rec.Code) | ||
| assert.Equal(t, "/_panel/", rec.Header().Get("Location")) | ||
| }) | ||
|
|
||
| t.Run("authenticated root GET passes through", func(t *testing.T) { | ||
| req := httptest.NewRequest(http.MethodGet, "/", nil) | ||
| req.Host = "example.com" | ||
| req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=AKIAEXAMPLE/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=abc") | ||
| rec := httptest.NewRecorder() | ||
| h.ServeHTTP(rec, req) | ||
| assert.Equal(t, http.StatusOK, rec.Code) | ||
| assert.Equal(t, "s3", rec.Body.String()) | ||
| }) | ||
|
|
||
| t.Run("presigned root GET passes through", func(t *testing.T) { | ||
| rec := run(http.MethodGet, | ||
| "/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAEXAMPLE/20260101/us-east-1/s3/aws4_request&X-Amz-Signature=abc", | ||
| "example.com") | ||
| assert.Equal(t, http.StatusOK, rec.Code) | ||
| assert.Equal(t, "s3", rec.Body.String()) | ||
| }) | ||
|
|
||
| t.Run("host-style bucket root GET passes through", func(t *testing.T) { | ||
| rec := run(http.MethodGet, "/", "mybucket.s3.example.com") | ||
| assert.Equal(t, http.StatusOK, rec.Code) | ||
| assert.Equal(t, "s3", rec.Body.String()) | ||
| }) | ||
|
|
||
| t.Run("root GET on host base apex redirects to panel", func(t *testing.T) { | ||
| rec := run(http.MethodGet, "/", "s3.example.com") | ||
| assert.Equal(t, http.StatusFound, rec.Code) | ||
| assert.Equal(t, "/_panel/", rec.Header().Get("Location")) | ||
| }) | ||
|
|
||
| t.Run("host-style localhost bucket root GET passes through", func(t *testing.T) { | ||
| rec := run(http.MethodGet, "/", "mybucket.localhost") | ||
| assert.Equal(t, http.StatusOK, rec.Code) | ||
| assert.Equal(t, "s3", rec.Body.String()) | ||
| }) | ||
|
|
||
| t.Run("non-root path passes through", func(t *testing.T) { | ||
| rec := run(http.MethodGet, "/mybucket", "example.com") | ||
| assert.Equal(t, http.StatusOK, rec.Code) | ||
| assert.Equal(t, "s3", rec.Body.String()) | ||
| }) | ||
|
|
||
| t.Run("non-GET root passes through", func(t *testing.T) { | ||
| rec := run(http.MethodHead, "/", "example.com") | ||
| assert.Equal(t, http.StatusOK, rec.Code) | ||
| assert.Equal(t, "s3", rec.Body.String()) | ||
| }) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.