From 2d3027eaba3c4926a69989c3ee2efa3246de94ea Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 3 Dec 2023 10:19:13 +0100 Subject: [PATCH 01/46] Update pull_request_template.md --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 675ea9ad68..323c265e79 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -30,4 +30,4 @@ Please delete options that are not relevant. ## Commit formatting: -Use emojis on commit messages so it provides an easy way of identifying the purpose or intention of a commit. Check out the emoji cheatsheet here: https://gitmoji.carloscuesta.me/ +Use emojis on commit messages so it provides an easy way of identifying the purpose or intention of a commit. Check out the emoji cheatsheet here: [CONTRIBUTING.md](https://github.com/gofiber/fiber/blob/master/.github/CONTRIBUTING.md#pull-requests-or-commits) From 9bcc7f3978ea81b6f5bbf759594cefed7ef4841d Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 3 Dec 2023 10:20:29 +0100 Subject: [PATCH 02/46] Update v3-changes.md --- .github/PULL_REQUEST_TEMPLATE/v3-changes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/v3-changes.md b/.github/PULL_REQUEST_TEMPLATE/v3-changes.md index 3f4b095767..250037be08 100644 --- a/.github/PULL_REQUEST_TEMPLATE/v3-changes.md +++ b/.github/PULL_REQUEST_TEMPLATE/v3-changes.md @@ -41,5 +41,4 @@ Before you submit your pull request, please make sure you meet these requirement ## Commit Formatting -Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. You can refer to the emoji cheatsheet here: https://gitmoji.carloscuesta.me/ - +Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: [CONTRIBUTING.md](https://github.com/gofiber/fiber/blob/master/.github/CONTRIBUTING.md#pull-requests-or-commits) From a80b288592d1a15616b2c8f944e8b8b63f4af8d9 Mon Sep 17 00:00:00 2001 From: tokelo-12 <113810058+tokelo-12@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:21:03 +0200 Subject: [PATCH 03/46] Update CONTRIBUTING.md (#2752) Grammar correction. --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d23c99af24..e35ce19942 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ Titles always we must use prefix according to below: - 📚 Doc: Translate to Portuguese middleware redirect - 🎨 Style: Respected pattern Golint -All pull request that contains a feature or fix is mandatory to have unit tests. Your PR is only to be merged if you respect this flow. +All pull requests that contain a feature or fix are mandatory to have unit tests. Your PR is only to be merged if you respect this flow. # 👍 Contribute From e4d7e84335e2b01bf3dfd03d13483bafa4700a3c Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Thu, 7 Dec 2023 03:39:41 -0400 Subject: [PATCH 04/46] chore(encryptcookie)!: update default config (#2753) * chore(encryptcookie)!: update default config docs(encryptcookie): enhance documentation and examples BREAKING CHANGE: removed the hardcoded "csrf_" from the Except. * docs(encryptcookie): reads or modifies cookies * chore(encryptcookie): csrf config example * docs(encryptcookie): md table spacing --- docs/api/middleware/encryptcookie.md | 58 ++++++++++++++++------------ middleware/encryptcookie/config.go | 2 +- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/docs/api/middleware/encryptcookie.md b/docs/api/middleware/encryptcookie.md index 743578bd87..c6e689e0e8 100644 --- a/docs/api/middleware/encryptcookie.md +++ b/docs/api/middleware/encryptcookie.md @@ -4,7 +4,11 @@ id: encryptcookie # Encrypt Cookie -Encrypt middleware for [Fiber](https://github.com/gofiber/fiber) which encrypts cookie values. Note: this middleware does not encrypt cookie names. +Encrypt Cookie is a middleware for [Fiber](https://github.com/gofiber/fiber) that secures your cookie values through encryption. + +:::note +This middleware encrypts cookie values and not the cookie names. +::: ## Signatures @@ -18,7 +22,7 @@ func GenerateKey() string ## Examples -Import the middleware package that is part of the Fiber web framework +To use the Encrypt Cookie middleware, first, import the middleware package as part of the Fiber web framework: ```go import ( @@ -27,23 +31,20 @@ import ( ) ``` -After you initiate your Fiber app, you can use the following possibilities: +Once you've imported the middleware package, you can use it inside your Fiber app: ```go -// Provide a minimal config -// `Key` must be a 32 character string. It's used to encrypt the values, so make sure it is random and keep it secret. -// You can run `openssl rand -base64 32` or call `encryptcookie.GenerateKey()` to create a random key for you. -// Make sure not to set `Key` to `encryptcookie.GenerateKey()` because that will create a new key every run. +// Provide a minimal configuration app.Use(encryptcookie.New(encryptcookie.Config{ Key: "secret-thirty-2-character-string", })) -// Get / reading out the encrypted cookie +// Retrieve the encrypted cookie value app.Get("/", func(c *fiber.Ctx) error { return c.SendString("value=" + c.Cookies("test")) }) -// Post / create the encrypted cookie +// Create an encrypted cookie app.Post("/", func(c *fiber.Ctx) error { c.Cookie(&fiber.Cookie{ Name: "test", @@ -53,39 +54,48 @@ app.Post("/", func(c *fiber.Ctx) error { }) ``` +:::note +`Key` must be a 32 character string. It's used to encrypt the values, so make sure it is random and keep it secret. +You can run `openssl rand -base64 32` or call `encryptcookie.GenerateKey()` to create a random key for you. +Make sure not to set `Key` to `encryptcookie.GenerateKey()` because that will create a new key every run. +::: + ## Config -| Property | Type | Description | Default | -|:----------|:----------------------------------------------------|:----------------------------------------------------------------------------------------------------|:-----------------------------| -| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| Except | `[]string` | Array of cookie keys that should not be encrypted. | `[]` | -| Key | `string` | Base64 encoded unique key to encode & decode cookies. Required. Key length should be 32 characters. | (No default, required field) | -| Encryptor | `func(decryptedString, key string) (string, error)` | Custom function to encrypt cookies. | `EncryptCookie` | -| Decryptor | `func(encryptedString, key string) (string, error)` | Custom function to decrypt cookies. | `DecryptCookie` | +| Property | Type | Description | Default | +|:----------|:----------------------------------------------------|:------------------------------------------------------------------------------------------------------|:-----------------------------| +| Next | `func(*fiber.Ctx) bool` | A function to skip this middleware when returned true. | `nil` | +| Except | `[]string` | Array of cookie keys that should not be encrypted. | `[]` | +| Key | `string` | A base64-encoded unique key to encode & decode cookies. Required. Key length should be 32 characters. | (No default, required field) | +| Encryptor | `func(decryptedString, key string) (string, error)` | A custom function to encrypt cookies. | `EncryptCookie` | +| Decryptor | `func(encryptedString, key string) (string, error)` | A custom function to decrypt cookies. | `DecryptCookie` | ## Default Config ```go var ConfigDefault = Config{ Next: nil, - Except: []string{"csrf_"}, + Except: []string{}, Key: "", Encryptor: EncryptCookie, Decryptor: DecryptCookie, } ``` -## Usage of CSRF and Encryptcookie Middlewares with Custom Cookie Names -Normally, encryptcookie middleware skips `csrf_` cookies. However, it won't work when you use custom cookie names for CSRF. You should update `Except` config to avoid this problem. For example: +## Usage With Other Middlewares That Reads Or Modify Cookies +Place the encryptcookie middleware before any other middleware that reads or modifies cookies. For example, if you are using the CSRF middleware, ensure that the encryptcookie middleware is placed before it. Failure to do so may prevent the CSRF middleware from reading the encrypted cookie. + +You may also choose to exclude certain cookies from encryption. For instance, if you are using the CSRF middleware with a frontend framework like Angular, and the framework reads the token from a cookie, you should exclude that cookie from encryption. This can be achieved by adding the cookie name to the Except array in the configuration: ```go app.Use(encryptcookie.New(encryptcookie.Config{ - Key: "secret-thirty-2-character-string", - Except: []string{"csrf_1"}, // exclude CSRF cookie + Key: "secret-thirty-2-character-string", + Except: []string{csrf.ConfigDefault.CookieName}, // exclude CSRF cookie })) app.Use(csrf.New(csrf.Config{ - KeyLookup: "form:test", - CookieName: "csrf_1", - CookieHTTPOnly: true, + KeyLookup: "header:" + csrf.HeaderName, + CookieSameSite: "Lax", + CookieSecure: true, + CookieHTTPOnly: false, })) ``` diff --git a/middleware/encryptcookie/config.go b/middleware/encryptcookie/config.go index c49fc16ebb..731ac07d79 100644 --- a/middleware/encryptcookie/config.go +++ b/middleware/encryptcookie/config.go @@ -36,7 +36,7 @@ type Config struct { // ConfigDefault is the default config var ConfigDefault = Config{ Next: nil, - Except: []string{"csrf_"}, + Except: []string{}, Key: "", Encryptor: EncryptCookie, Decryptor: DecryptCookie, From 8aa88689ae276affecb50956120df54a16eb2b3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:14:33 +0100 Subject: [PATCH 05/46] build(deps): bump actions/setup-go from 4 to 5 (#2754) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/benchmark.yml | 2 +- .github/workflows/linter.yml | 2 +- .github/workflows/test.yml | 2 +- .github/workflows/vulncheck.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index cbef15c3b3..7625373255 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: # NOTE: Keep this in sync with the version from go.mod go-version: "1.20.x" diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 37c53168a5..3defb96344 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: # NOTE: Keep this in sync with the version from go.mod go-version: "1.20.x" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c11960f478..f103d914fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index 85e96fdd00..e6d29ccb4b 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: "stable" check-latest: true From c441bdf2d026e890df21ae3aa89c8e6a638df3c5 Mon Sep 17 00:00:00 2001 From: iRedMail <2048991+iredmail@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:36:43 +0800 Subject: [PATCH 06/46] =?UTF-8?q?=F0=9F=A9=B9=20middleware/logger/:=20log?= =?UTF-8?q?=20client=20IP=20address=20by=20default=20(#2755)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * middleware/logger: Log client IP address by default. * Update doc. --- docs/api/middleware/logger.md | 4 ++-- middleware/logger/config.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api/middleware/logger.md b/docs/api/middleware/logger.md index a01e9bd54c..97c3c73872 100644 --- a/docs/api/middleware/logger.md +++ b/docs/api/middleware/logger.md @@ -97,7 +97,7 @@ app.Use(logger.New(logger.Config{ | Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | | Done | `func(*fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | | CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | -| Format | `string` | Format defines the logging tags. | `[${time}] ${status} - ${latency} ${method} ${path}\n` | +| Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path}\n` | | TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | | TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | | TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | @@ -112,7 +112,7 @@ app.Use(logger.New(logger.Config{ var ConfigDefault = Config{ Next: nil, Done: nil, - Format: "[${time}] ${status} - ${latency} ${method} ${path}\n", + Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path}\n", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 5b91a065ee..10b382b925 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -28,7 +28,7 @@ type Config struct { // Format defines the logging tags // - // Optional. Default: [${time}] ${status} - ${latency} ${method} ${path}\n + // Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path}\n Format string // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html @@ -86,7 +86,7 @@ type LogFunc func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (i var ConfigDefault = Config{ Next: nil, Done: nil, - Format: "[${time}] ${status} - ${latency} ${method} ${path}\n", + Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path}\n", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, From b1850834a341da80691e23afc5568b7a053fdffa Mon Sep 17 00:00:00 2001 From: Benjamin Grosse Date: Tue, 12 Dec 2023 05:55:29 -0800 Subject: [PATCH 07/46] fix: don't constrain middlewares' context-keys to strings :bug: (#2751) * Revert "Revert ":bug: requestid.Config.ContextKey is interface{} (#2369)" (#2742)" This reverts commit 28be17f929cfa7d3c27dd292fc3956f2f9882e22. * fix: request ContextKey default value condition Should check for `nil` since it is `any`. * fix: don't constrain middlewares' context-keys to strings `context` recommends using "unexported type" as context keys to avoid collisions https://pkg.go.dev/github.com/gofiber/fiber/v2#Ctx.Locals. The official go blog also recommends this https://go.dev/blog/context. `fiber.Ctx.Locals(key any, value any)` correctly allows consumers to use unexported types or e.g. strings. But some fiber middlewares constrain their context-keys to `string` in their "default config structs", making it impossible to use unexported types. This PR removes the `string` _constraint_ from all middlewares, allowing to now use unexported types as per the official guidelines. However the default value is still a string, so it's not a breaking change, and anyone still using strings as context keys is not affected. --- docs/api/middleware/basicauth.md | 4 ++-- docs/api/middleware/csrf.md | 4 ++-- docs/api/middleware/keyauth.md | 2 +- docs/api/middleware/requestid.md | 2 +- middleware/adaptor/adaptor_test.go | 5 +++-- middleware/basicauth/config.go | 8 +++---- middleware/csrf/config.go | 6 ++--- middleware/csrf/csrf.go | 2 +- middleware/idempotency/idempotency.go | 6 +++-- middleware/keyauth/config.go | 4 ++-- middleware/requestid/config.go | 8 ++++--- middleware/requestid/requestid_test.go | 31 +++++++++++++++++++++++--- 12 files changed, 56 insertions(+), 26 deletions(-) diff --git a/docs/api/middleware/basicauth.md b/docs/api/middleware/basicauth.md index 0e90eafed0..d0f3609bde 100644 --- a/docs/api/middleware/basicauth.md +++ b/docs/api/middleware/basicauth.md @@ -67,8 +67,8 @@ app.Use(basicauth.New(basicauth.Config{ | Realm | `string` | Realm is a string to define the realm attribute of BasicAuth. The realm identifies the system to authenticate against and can be used by clients to save credentials. | `"Restricted"` | | Authorizer | `func(string, string) bool` | Authorizer defines a function to check the credentials. It will be called with a username and password and is expected to return true or false to indicate approval. | `nil` | | Unauthorized | `fiber.Handler` | Unauthorized defines the response body for unauthorized responses. | `nil` | -| ContextUsername | `string` | ContextUsername is the key to store the username in Locals. | `"username"` | -| ContextPassword | `string` | ContextPassword is the key to store the password in Locals. | `"password"` | +| ContextUsername | `interface{}` | ContextUsername is the key to store the username in Locals. | `"username"` | +| ContextPassword | `interface{}` | ContextPassword is the key to store the password in Locals. | `"password"` | ## Default Config diff --git a/docs/api/middleware/csrf.md b/docs/api/middleware/csrf.md index 2c30ed5cbe..536e8a4b2d 100644 --- a/docs/api/middleware/csrf.md +++ b/docs/api/middleware/csrf.md @@ -152,14 +152,14 @@ app.Use(csrf.New(csrf.Config{ | Storage | `fiber.Storage` | Store is used to store the state of the middleware. | `nil` | | Session | `*session.Store` | Session is used to store the state of the middleware. Overrides Storage if set. | `nil` | | SessionKey | `string` | SessionKey is the key used to store the token within the session. | "fiber.csrf.token" | -| ContextKey | `string` | Context key to store the generated CSRF token into the context. If left empty, the token will not be stored within the context. | "" | +| ContextKey | `inteface{}` | Context key to store the generated CSRF token into the context. If left empty, the token will not be stored within the context. | "" | | KeyGenerator | `func() string` | KeyGenerator creates a new CSRF token. | utils.UUID | | CookieExpires | `time.Duration` (Deprecated) | Deprecated: Please use Expiration. | 0 | | Cookie | `*fiber.Cookie` (Deprecated) | Deprecated: Please use Cookie* related fields. | `nil` | | TokenLookup | `string` (Deprecated) | Deprecated: Please use KeyLookup. | "" | | ErrorHandler | `fiber.ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. | DefaultErrorHandler | | Extractor | `func(*fiber.Ctx) (string, error)` | Extractor returns the CSRF token. If set, this will be used in place of an Extractor based on KeyLookup. | Extractor based on KeyLookup | -| HandlerContextKey | `string` | HandlerContextKey is used to store the CSRF Handler into context. | "fiber.csrf.handler" | +| HandlerContextKey | `interface{}` | HandlerContextKey is used to store the CSRF Handler into context. | "fiber.csrf.handler" | ### Default Config diff --git a/docs/api/middleware/keyauth.md b/docs/api/middleware/keyauth.md index ecabe122e7..1a719c134d 100644 --- a/docs/api/middleware/keyauth.md +++ b/docs/api/middleware/keyauth.md @@ -221,7 +221,7 @@ curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000 | KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. | "header:Authorization" | | AuthScheme | `string` | AuthScheme to be used in the Authorization header. | "Bearer" | | Validator | `func(*fiber.Ctx, string) (bool, error)` | Validator is a function to validate the key. | A function for key validation | -| ContextKey | `string` | Context key to store the bearer token from the token into context. | "token" | +| ContextKey | `interface{}` | Context key to store the bearer token from the token into context. | "token" | ## Default Config diff --git a/docs/api/middleware/requestid.md b/docs/api/middleware/requestid.md index 112d21d48a..c8ca8d599e 100644 --- a/docs/api/middleware/requestid.md +++ b/docs/api/middleware/requestid.md @@ -45,7 +45,7 @@ app.Use(requestid.New(requestid.Config{ | Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | | Header | `string` | Header is the header key where to get/set the unique request ID. | "X-Request-ID" | | Generator | `func() string` | Generator defines a function to generate the unique identifier. | utils.UUID | -| ContextKey | `string` | ContextKey defines the key used when storing the request ID in the locals for a specific request. | "requestid" | +| ContextKey | `interface{}` | ContextKey defines the key used when storing the request ID in the locals for a specific request. | "requestid" | ## Default Config The default config uses a fast UUID generator which will expose the number of diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index 340c05c04b..6e03b05a2d 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -36,7 +36,8 @@ func Test_HTTPHandler(t *testing.T) { expectedURL, err := url.ParseRequestURI(expectedRequestURI) utils.AssertEqual(t, nil, err) - expectedContextKey := "contextKey" + type contextKeyType string + expectedContextKey := contextKeyType("contextKey") expectedContextValue := "contextValue" callsCount := 0 @@ -293,7 +294,7 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A utils.AssertEqual(t, expectedResponseBody, string(w.body), "Body") } -func setFiberContextValueMiddleware(next fiber.Handler, key string, value interface{}) fiber.Handler { +func setFiberContextValueMiddleware(next fiber.Handler, key, value interface{}) fiber.Handler { return func(c *fiber.Ctx) error { c.Locals(key, value) return next(c) diff --git a/middleware/basicauth/config.go b/middleware/basicauth/config.go index 3845e91538..d69f48be67 100644 --- a/middleware/basicauth/config.go +++ b/middleware/basicauth/config.go @@ -44,12 +44,12 @@ type Config struct { // ContextUser is the key to store the username in Locals // // Optional. Default: "username" - ContextUsername string + ContextUsername interface{} // ContextPass is the key to store the password in Locals // // Optional. Default: "password" - ContextPassword string + ContextPassword interface{} } // ConfigDefault is the default config @@ -95,10 +95,10 @@ func configDefault(config ...Config) Config { return c.SendStatus(fiber.StatusUnauthorized) } } - if cfg.ContextUsername == "" { + if cfg.ContextUsername == nil { cfg.ContextUsername = ConfigDefault.ContextUsername } - if cfg.ContextPassword == "" { + if cfg.ContextPassword == nil { cfg.ContextPassword = ConfigDefault.ContextPassword } return cfg diff --git a/middleware/csrf/config.go b/middleware/csrf/config.go index 539e84967c..9ab6cab063 100644 --- a/middleware/csrf/config.go +++ b/middleware/csrf/config.go @@ -93,7 +93,7 @@ type Config struct { // If left empty, token will not be stored in context. // // Optional. Default: "" - ContextKey string + ContextKey interface{} // KeyGenerator creates a new CSRF token // @@ -124,7 +124,7 @@ type Config struct { // HandlerContextKey is used to store the CSRF Handler into context // // Default: "fiber.csrf.handler" - HandlerContextKey string + HandlerContextKey interface{} } const HeaderName = "X-Csrf-Token" @@ -204,7 +204,7 @@ func configDefault(config ...Config) Config { if cfg.SessionKey == "" { cfg.SessionKey = ConfigDefault.SessionKey } - if cfg.HandlerContextKey == "" { + if cfg.HandlerContextKey == nil { cfg.HandlerContextKey = ConfigDefault.HandlerContextKey } diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index f4dee74c8e..939daaea79 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -129,7 +129,7 @@ func New(config ...Config) fiber.Handler { c.Vary(fiber.HeaderCookie) // Store the token in the context if a context key is specified - if cfg.ContextKey != "" { + if cfg.ContextKey != nil { c.Locals(cfg.ContextKey, token) } diff --git a/middleware/idempotency/idempotency.go b/middleware/idempotency/idempotency.go index 604f867c76..5affc59620 100644 --- a/middleware/idempotency/idempotency.go +++ b/middleware/idempotency/idempotency.go @@ -12,9 +12,11 @@ import ( // Inspired by https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-02 // and https://github.com/penguin-statistics/backend-next/blob/f2f7d5ba54fc8a58f168d153baa17b2ad4a14e45/internal/pkg/middlewares/idempotency.go +type localsKeys string + const ( - localsKeyIsFromCache = "idempotency_isfromcache" - localsKeyWasPutToCache = "idempotency_wasputtocache" + localsKeyIsFromCache localsKeys = "idempotency_isfromcache" + localsKeyWasPutToCache localsKeys = "idempotency_wasputtocache" ) func IsFromCache(c *fiber.Ctx) bool { diff --git a/middleware/keyauth/config.go b/middleware/keyauth/config.go index 39d71b6305..c762d72ce6 100644 --- a/middleware/keyauth/config.go +++ b/middleware/keyauth/config.go @@ -41,7 +41,7 @@ type Config struct { // Context key to store the bearertoken from the token into context. // Optional. Default: "token". - ContextKey string + ContextKey interface{} } // ConfigDefault is the default config @@ -87,7 +87,7 @@ func configDefault(config ...Config) Config { if cfg.Validator == nil { panic("fiber: keyauth middleware requires a validator function") } - if cfg.ContextKey == "" { + if cfg.ContextKey == nil { cfg.ContextKey = ConfigDefault.ContextKey } diff --git a/middleware/requestid/config.go b/middleware/requestid/config.go index b3b605e590..b535ec9033 100644 --- a/middleware/requestid/config.go +++ b/middleware/requestid/config.go @@ -24,9 +24,11 @@ type Config struct { // ContextKey defines the key used when storing the request ID in // the locals for a specific request. + // Should be a private type instead of string, but too many apps probably + // rely on this exact value. // - // Optional. Default: requestid - ContextKey string + // Optional. Default: "requestid" + ContextKey interface{} } // ConfigDefault is the default config @@ -57,7 +59,7 @@ func configDefault(config ...Config) Config { if cfg.Generator == nil { cfg.Generator = ConfigDefault.Generator } - if cfg.ContextKey == "" { + if cfg.ContextKey == nil { cfg.ContextKey = ConfigDefault.ContextKey } return cfg diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index 451e96b4b2..b2dc2ac7bf 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -55,20 +55,45 @@ func Test_RequestID_Next(t *testing.T) { func Test_RequestID_Locals(t *testing.T) { t.Parallel() reqID := "ThisIsARequestId" - ctxKey := "ThisIsAContextKey" + type ContextKey int + const requestContextKey ContextKey = iota app := fiber.New() app.Use(New(Config{ Generator: func() string { return reqID }, - ContextKey: ctxKey, + ContextKey: requestContextKey, })) var ctxVal string app.Use(func(c *fiber.Ctx) error { - ctxVal = c.Locals(ctxKey).(string) //nolint:forcetypeassert,errcheck // We always store a string in here + ctxVal = c.Locals(requestContextKey).(string) //nolint:forcetypeassert,errcheck // We always store a string in here + return c.Next() + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, reqID, ctxVal) +} + +// go test -run Test_RequestID_DefaultKey +func Test_RequestID_DefaultKey(t *testing.T) { + t.Parallel() + reqID := "ThisIsARequestId" + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return reqID + }, + })) + + var ctxVal string + + app.Use(func(c *fiber.Ctx) error { + ctxVal = c.Locals("requestid").(string) //nolint:forcetypeassert,errcheck // We always store a string in here return c.Next() }) From 090acbbc5d50a06a678620fa87352ea2530b9906 Mon Sep 17 00:00:00 2001 From: Mehmet Firat KOMURCU Date: Wed, 13 Dec 2023 10:52:16 +0300 Subject: [PATCH 08/46] =?UTF-8?q?=F0=9F=93=9A=20Update=20app.md=20for=20in?= =?UTF-8?q?dentation=20(#2761)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update app.md for indentation --- docs/api/app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/app.md b/docs/api/app.md index 93dae1bc64..198c83ce1d 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -205,7 +205,7 @@ func main() { app.Route("/test", func(api fiber.Router) { api.Get("/foo", handler).Name("foo") // /test/foo (name: test.foo) - api.Get("/bar", handler).Name("bar") // /test/bar (name: test.bar) + api.Get("/bar", handler).Name("bar") // /test/bar (name: test.bar) }, "test.") log.Fatal(app.Listen(":3000")) From c3fed55744190762959f6ae771ec3401eb0e3a5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:53:13 +0300 Subject: [PATCH 09/46] build(deps): bump github.com/google/uuid from 1.4.0 to 1.5.0 (#2762) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index eac01a31fc..4112a1c4f1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gofiber/fiber/v2 go 1.20 require ( - github.com/google/uuid v1.4.0 + github.com/google/uuid v1.5.0 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-runewidth v0.0.15 diff --git a/go.sum b/go.sum index 56bcdc96b3..f000e1cacb 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= From b0925dc454b00f447ced2b51804eafed048ebf4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Dec 2023 13:30:45 +0300 Subject: [PATCH 10/46] build(deps): bump github/codeql-action from 2 to 3 (#2763) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bedc27fe61..82036cc967 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -37,7 +37,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 # Override language selection by uncommenting this and choosing your languages with: languages: go @@ -45,7 +45,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -59,4 +59,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From dc2d2ef52429887613c85e6177646a680c723fff Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 18 Dec 2023 11:20:18 -0300 Subject: [PATCH 11/46] Changing default log output (#2730) changing default log output Closes #2729 --- docs/api/middleware/logger.md | 4 +-- middleware/logger/config.go | 4 +-- middleware/logger/logger.go | 53 -------------------------------- middleware/logger/logger_test.go | 2 +- 4 files changed, 5 insertions(+), 58 deletions(-) diff --git a/docs/api/middleware/logger.md b/docs/api/middleware/logger.md index 97c3c73872..d9e4fadad8 100644 --- a/docs/api/middleware/logger.md +++ b/docs/api/middleware/logger.md @@ -97,7 +97,7 @@ app.Use(logger.New(logger.Config{ | Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | | Done | `func(*fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | | CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | -| Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path}\n` | +| Format | `string` | Format defines the logging tags. | `${time} | ${status} | ${latency} | ${ip} | ${method} | ${path}` | | TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | | TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | | TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | @@ -112,7 +112,7 @@ app.Use(logger.New(logger.Config{ var ConfigDefault = Config{ Next: nil, Done: nil, - Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path}\n", + Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path}", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 10b382b925..06a1a41846 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -28,7 +28,7 @@ type Config struct { // Format defines the logging tags // - // Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path}\n + // Optional. Default: ${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} Format string // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html @@ -86,7 +86,7 @@ type LogFunc func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (i var ConfigDefault = Config{ Next: nil, Done: nil, - Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path}\n", + Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path}\n", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index f33f3562a3..b03617b2b3 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -85,9 +85,6 @@ func New(config ...Config) fiber.Handler { return c.Next() } - // Alias colors - colors := c.App().Config().ColorScheme - // Set error handler once once.Do(func() { // get longested possible path @@ -137,56 +134,6 @@ func New(config ...Config) fiber.Handler { // Get new buffer buf := bytebufferpool.Get() - // Default output when no custom Format or io.Writer is given - if cfg.Format == ConfigDefault.Format { - // Format error if exist - formatErr := "" - if cfg.enableColors { - if chainErr != nil { - formatErr = colors.Red + " | " + chainErr.Error() + colors.Reset - } - _, _ = buf.WriteString( //nolint:errcheck // This will never fail - fmt.Sprintf("%s |%s %3d %s| %13v | %15s |%s %-7s %s| %-"+errPaddingStr+"s %s\n", - timestamp.Load().(string), - statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset, - data.Stop.Sub(data.Start), - c.IP(), - methodColor(c.Method(), colors), c.Method(), colors.Reset, - c.Path(), - formatErr, - ), - ) - } else { - if chainErr != nil { - formatErr = " | " + chainErr.Error() - } - _, _ = buf.WriteString( //nolint:errcheck // This will never fail - fmt.Sprintf("%s | %3d | %13v | %15s | %-7s | %-"+errPaddingStr+"s %s\n", - timestamp.Load().(string), - c.Response().StatusCode(), - data.Stop.Sub(data.Start), - c.IP(), - c.Method(), - c.Path(), - formatErr, - ), - ) - } - - // Write buffer to output - _, _ = cfg.Output.Write(buf.Bytes()) //nolint:errcheck // This will never fail - - if cfg.Done != nil { - cfg.Done(c, buf.Bytes()) - } - - // Put buffer back to pool - bytebufferpool.Put(buf) - - // End chain - return nil - } - var err error // Loop over template parts execute dynamic parts and add fixed parts to the buffer for i, logFunc := range logFunChain { diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index d563a5ccc6..6876a7219f 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -147,7 +147,7 @@ type fakeOutput int func (o *fakeOutput) Write([]byte) (int, error) { *o++ - return 0, errors.New("fake output") + return 0, nil } // go test -run Test_Logger_ErrorOutput_WithoutColor From 43fa236d991bfb090a695445275f13ceefff80a3 Mon Sep 17 00:00:00 2001 From: RW Date: Tue, 19 Dec 2023 13:27:10 +0100 Subject: [PATCH 12/46] Update hooks.md fix wrong hooks signature --- docs/guide/hooks.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md index b001786535..357d59d33d 100644 --- a/docs/guide/hooks.md +++ b/docs/guide/hooks.md @@ -35,7 +35,7 @@ type OnMountHandler = func(*App) error OnRoute is a hook to execute user functions on each route registeration. Also you can get route properties by **route** parameter. ```go title="Signature" -func (app *App) OnRoute(handler ...OnRouteHandler) +func (h *Hooks) OnRoute(handler ...OnRouteHandler) ``` ## OnName @@ -47,7 +47,7 @@ OnName only works with naming routes, not groups. ::: ```go title="Signature" -func (app *App) OnName(handler ...OnNameHandler) +func (h *Hooks) OnName(handler ...OnNameHandler) ``` @@ -104,7 +104,7 @@ func main() { OnGroup is a hook to execute user functions on each group registeration. Also you can get group properties by **group** parameter. ```go title="Signature" -func (app *App) OnGroup(handler ...OnGroupHandler) +func (h *Hooks) OnGroup(handler ...OnGroupHandler) ``` ## OnGroupName @@ -116,7 +116,7 @@ OnGroupName only works with naming groups, not routes. ::: ```go title="Signature" -func (app *App) OnGroupName(handler ...OnGroupNameHandler) +func (h *Hooks) OnGroupName(handler ...OnGroupNameHandler) ``` ## OnListen @@ -124,7 +124,7 @@ func (app *App) OnGroupName(handler ...OnGroupNameHandler) OnListen is a hook to execute user functions on Listen, ListenTLS, Listener. ```go title="Signature" -func (app *App) OnListen(handler ...OnListenHandler) +func (h *Hooks) OnListen(handler ...OnListenHandler) ``` @@ -158,7 +158,7 @@ app.Listen(":5000") OnFork is a hook to execute user functions on Fork. ```go title="Signature" -func (app *App) OnFork(handler ...OnForkHandler) +func (h *Hooks) OnFork(handler ...OnForkHandler) ``` ## OnShutdown @@ -166,7 +166,7 @@ func (app *App) OnFork(handler ...OnForkHandler) OnShutdown is a hook to execute user functions after Shutdown. ```go title="Signature" -func (app *App) OnShutdown(handler ...OnShutdownHandler) +func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) ``` ## OnMount From 1fac52a42af3f9a4981de71e27b4edd6090dac31 Mon Sep 17 00:00:00 2001 From: Muhammad Kholid B Date: Fri, 22 Dec 2023 20:48:37 +0700 Subject: [PATCH 13/46] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20CORS=20middleware?= =?UTF-8?q?=20should=20use=20the=20defined=20AllowedOriginsFunc=20config?= =?UTF-8?q?=20when=20AllowedOrigins=20is=20empty=20(#2771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/cors/cors.go | 7 +- middleware/cors/cors_test.go | 201 ++++++++++++++++++++++++++++++++++- 2 files changed, 202 insertions(+), 6 deletions(-) diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index c347e43525..ebc1c6b1cb 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -94,13 +94,14 @@ func New(config ...Config) fiber.Handler { if cfg.AllowMethods == "" { cfg.AllowMethods = ConfigDefault.AllowMethods } - if cfg.AllowOrigins == "" { + // When none of the AllowOrigins or AllowOriginsFunc config was defined, set the default AllowOrigins value with "*" + if cfg.AllowOrigins == "" && cfg.AllowOriginsFunc == nil { cfg.AllowOrigins = ConfigDefault.AllowOrigins } } // Warning logs if both AllowOrigins and AllowOriginsFunc are set - if cfg.AllowOrigins != ConfigDefault.AllowOrigins && cfg.AllowOriginsFunc != nil { + if cfg.AllowOrigins != "" && cfg.AllowOriginsFunc != nil { log.Warn("[CORS] Both 'AllowOrigins' and 'AllowOriginsFunc' have been defined.") } @@ -145,7 +146,7 @@ func New(config ...Config) fiber.Handler { // Run AllowOriginsFunc if the logic for // handling the value in 'AllowOrigins' does // not result in allowOrigin being set. - if (allowOrigin == "" || allowOrigin == ConfigDefault.AllowOrigins) && cfg.AllowOriginsFunc != nil { + if allowOrigin == "" && cfg.AllowOriginsFunc != nil { if cfg.AllowOriginsFunc(origin) { allowOrigin = origin } diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index 692a24bfcc..22fef8442b 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -331,9 +331,9 @@ func Test_CORS_AllowOriginsFunc(t *testing.T) { // Perform request handler(ctx) - // Allow-Origin header should be "*" because http://google.com does not satisfy 'strings.Contains(origin, "example-2")' - // and AllowOrigins has not been set so the default "*" is used - utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + // Allow-Origin header should be empty because http://google.com does not satisfy 'strings.Contains(origin, "example-2")' + // and AllowOrigins has not been set + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) ctx.Request.Reset() ctx.Response.Reset() @@ -348,3 +348,198 @@ func Test_CORS_AllowOriginsFunc(t *testing.T) { // Allow-Origin header should be "http://example-2.com" utils.AssertEqual(t, "http://example-2.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) } + +func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { + testCases := []struct { + Name string + Config Config + RequestOrigin string + ResponseOrigin string + }{ + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(origin string) bool { + return true + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(origin string) bool { + return true + }, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "http://bbb.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(origin string) bool { + return false + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(origin string) bool { + return false + }, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "", + }, + { + Name: "AllowOriginsEmpty/AllowOriginsFuncUndefined/OriginAllowed", + Config: Config{ + AllowOrigins: "", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "*", + }, + { + Name: "AllowOriginsEmpty/AllowOriginsFuncReturnsTrue/OriginAllowed", + Config: Config{ + AllowOrigins: "", + AllowOriginsFunc: func(origin string) bool { + return true + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsEmpty/AllowOriginsFuncReturnsFalse/OriginNotAllowed", + Config: Config{ + AllowOrigins: "", + AllowOriginsFunc: func(origin string) bool { + return false + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + app := fiber.New() + app.Use("/", New(tc.Config)) + + handler := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin) + + handler(ctx) + + utils.AssertEqual(t, tc.ResponseOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + }) + } +} + +// The fix for issue #2422 +func Test_CORS_AllowCredetials(t *testing.T) { + testCases := []struct { + Name string + Config Config + RequestOrigin string + ResponseOrigin string + }{ + { + Name: "AllowOriginsFuncDefined", + Config: Config{ + AllowCredentials: true, + AllowOriginsFunc: func(origin string) bool { + return true + }, + }, + RequestOrigin: "http://aaa.com", + // The AllowOriginsFunc config was defined, should use the real origin of the function + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsFuncNotDefined", + Config: Config{ + AllowCredentials: true, + }, + RequestOrigin: "http://aaa.com", + // None of the AllowOrigins or AllowOriginsFunc config was defined, should use the default origin of "*" + // which will cause the CORS error in the client: + // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' + // when the request's credentials mode is 'include'. + ResponseOrigin: "*", + }, + { + Name: "AllowOriginsDefined", + Config: Config{ + AllowCredentials: true, + AllowOrigins: "http://aaa.com", + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/UnallowedOrigin", + Config: Config{ + AllowCredentials: true, + AllowOrigins: "http://aaa.com", + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + app := fiber.New() + app.Use("/", New(tc.Config)) + + handler := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin) + + handler(ctx) + + if tc.Config.AllowCredentials { + utils.AssertEqual(t, "true", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + } + utils.AssertEqual(t, tc.ResponseOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + }) + } +} From c49faf9a8ae63a09fdec77b2b17972c447a92116 Mon Sep 17 00:00:00 2001 From: RW Date: Fri, 22 Dec 2023 14:49:58 +0100 Subject: [PATCH 14/46] =?UTF-8?q?=F0=9F=90=9B=20[Bug]:=20Adaptator=20+=20o?= =?UTF-8?q?telfiber=20issue=20#2641=20(#2772)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/adaptor/adaptor.go | 2 ++ middleware/adaptor/adaptor_test.go | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/middleware/adaptor/adaptor.go b/middleware/adaptor/adaptor.go index db2149d921..8bd6f3fe00 100644 --- a/middleware/adaptor/adaptor.go +++ b/middleware/adaptor/adaptor.go @@ -76,6 +76,7 @@ func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { c.Request().Header.SetMethod(r.Method) c.Request().SetRequestURI(r.RequestURI) c.Request().SetHost(r.Host) + c.Request().Header.SetHost(r.Host) for key, val := range r.Header { for _, v := range val { c.Request().Header.Set(key, v) @@ -128,6 +129,7 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { req.Header.SetMethod(r.Method) req.SetRequestURI(r.RequestURI) req.SetHost(r.Host) + req.Header.SetHost(r.Host) for key, val := range r.Header { for _, v := range val { req.Header.Set(key, v) diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index 6e03b05a2d..dc52704760 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -116,6 +116,7 @@ var ( ) func Test_HTTPMiddleware(t *testing.T) { + const expectedHost = "foobar.com" tests := []struct { name string url string @@ -148,6 +149,7 @@ func Test_HTTPMiddleware(t *testing.T) { w.WriteHeader(http.StatusMethodNotAllowed) return } + r = r.WithContext(context.WithValue(r.Context(), TestContextKey, "okay")) r = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, "not_okay")) r = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, "okay")) @@ -180,6 +182,7 @@ func Test_HTTPMiddleware(t *testing.T) { for _, tt := range tests { req, err := http.NewRequestWithContext(context.Background(), tt.method, tt.url, nil) + req.Host = expectedHost utils.AssertEqual(t, nil, err) resp, err := app.Test(req) @@ -188,6 +191,7 @@ func Test_HTTPMiddleware(t *testing.T) { } req, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, "/", nil) + req.Host = expectedHost utils.AssertEqual(t, nil, err) resp, err := app.Test(req) @@ -239,6 +243,8 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A utils.AssertEqual(t, expectedRequestURI, string(c.Context().RequestURI()), "RequestURI") utils.AssertEqual(t, expectedContentLength, c.Context().Request.Header.ContentLength(), "ContentLength") utils.AssertEqual(t, expectedHost, c.Hostname(), "Host") + utils.AssertEqual(t, expectedHost, string(c.Request().Header.Host()), "Host") + utils.AssertEqual(t, "http://"+expectedHost, c.BaseURL(), "BaseURL") utils.AssertEqual(t, expectedRemoteAddr, c.Context().RemoteAddr().String(), "RemoteAddr") body := string(c.Body()) From d6c88764ebaf0e48b98eaa1069168a9e00d6aa98 Mon Sep 17 00:00:00 2001 From: gilwo Date: Fri, 22 Dec 2023 17:18:04 +0200 Subject: [PATCH 15/46] =?UTF-8?q?=F0=9F=A9=B9=F0=9F=9A=A8=20-=20fix=20for?= =?UTF-8?q?=20redirect=20with=20query=20params=20(#2748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * redirect with query params did not work, fix it and add test for it * redirect middleware - fix test typo --- middleware/redirect/redirect.go | 6 +++++- middleware/redirect/redirect_test.go | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/middleware/redirect/redirect.go b/middleware/redirect/redirect.go index 7af21320fe..2818142e54 100644 --- a/middleware/redirect/redirect.go +++ b/middleware/redirect/redirect.go @@ -30,7 +30,11 @@ func New(config ...Config) fiber.Handler { for k, v := range cfg.rulesRegex { replacer := captureTokens(k, c.Path()) if replacer != nil { - return c.Redirect(replacer.Replace(v), cfg.StatusCode) + queryString := string(c.Context().QueryArgs().QueryString()) + if queryString != "" { + queryString = "?" + queryString + } + return c.Redirect(replacer.Replace(v)+queryString, cfg.StatusCode) } } return c.Next() diff --git a/middleware/redirect/redirect_test.go b/middleware/redirect/redirect_test.go index b6323ab54b..61fc00a11a 100644 --- a/middleware/redirect/redirect_test.go +++ b/middleware/redirect/redirect_test.go @@ -44,6 +44,12 @@ func Test_Redirect(t *testing.T) { }, StatusCode: fiber.StatusMovedPermanently, })) + app.Use(New(Config{ + Rules: map[string]string{ + "/params": "/with_params", + }, + StatusCode: fiber.StatusMovedPermanently, + })) app.Get("/api/*", func(c *fiber.Ctx) error { return c.SendString("API") @@ -104,6 +110,12 @@ func Test_Redirect(t *testing.T) { url: "/api/test", statusCode: fiber.StatusOK, }, + { + name: "redirect with query params", + url: "/params?query=abc", + redirectTo: "/with_params?query=abc", + statusCode: fiber.StatusMovedPermanently, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 38eb4bd238dda7c8b1eedd628267b5bb5263a6d4 Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 23 Dec 2023 08:31:58 +0100 Subject: [PATCH 16/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20logger/middleware=20?= =?UTF-8?q?colorize=20logger=20error=20message=20#2593=20(#2773)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/middleware/logger.md | 30 +++++++++++++++--------------- middleware/logger/config.go | 4 ++-- middleware/logger/tags.go | 4 ++++ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/docs/api/middleware/logger.md b/docs/api/middleware/logger.md index d9e4fadad8..0a23441aa3 100644 --- a/docs/api/middleware/logger.md +++ b/docs/api/middleware/logger.md @@ -92,27 +92,27 @@ app.Use(logger.New(logger.Config{ ### Config -| Property | Type | Description | Default | -|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------| -| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| Done | `func(*fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | -| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | -| Format | `string` | Format defines the logging tags. | `${time} | ${status} | ${latency} | ${ip} | ${method} | ${path}` | -| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | -| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | -| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | -| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` | -| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | -| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - | -| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - | -| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - | +| Property | Type | Description | Default | +|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:-------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Done | `func(*fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | +| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | +| Format | `string` | Format defines the logging tags. | `${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n` | +| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | +| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | +| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | +| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` | +| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | +| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - | +| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - | +| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - | ## Default Config ```go var ConfigDefault = Config{ Next: nil, Done: nil, - Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path}", + Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 06a1a41846..b84714cad5 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -28,7 +28,7 @@ type Config struct { // Format defines the logging tags // - // Optional. Default: ${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} + // Optional. Default: ${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n Format string // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html @@ -86,7 +86,7 @@ type LogFunc func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (i var ConfigDefault = Config{ Next: nil, Done: nil, - Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path}\n", + Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index 67ccbb83a2..9a10279eca 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -139,6 +139,10 @@ func createTagMap(cfg *Config) map[string]LogFunc { }, TagError: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { if data.ChainErr != nil { + if cfg.enableColors { + colors := c.App().Config().ColorScheme + return output.WriteString(fmt.Sprintf("%s%s%s", colors.Red, data.ChainErr.Error(), colors.Reset)) + } return output.WriteString(data.ChainErr.Error()) } return output.WriteString("-") From 6249bc48bcc9359e48cd5fb826a00ba485def6f0 Mon Sep 17 00:00:00 2001 From: Lucas Lemos Date: Wed, 3 Jan 2024 14:13:58 -0300 Subject: [PATCH 17/46] :sparkles: feat: add liveness and readiness checks (#2509) * :sparkles: feat: add liveness and readiness checkers * :memo: docs: add docs for liveness and readiness * :sparkles: feat: add options method for probe checkers * :white_check_mark: tests: add tests for liveness and readiness * :recycle: refactor: change default endpoint values * :recycle: refactor: change default value for liveness endpoint * :memo: docs: add return status for liveness and readiness probes * :recycle: refactor: change probechecker to middleware * :memo: docs: move docs to middleware session * :recycle: refactor: apply gofumpt formatting * :recycle: refactor: remove unused parameter * split config and apply a review * apply reviews and add testcases * add benchmark * cleanup * rename middleware * fix linter * Update docs and config values * Revert change to IsReady * Updates based on code review * Update docs to match other middlewares --------- Co-authored-by: Muhammed Efe Cetin Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez --- docs/api/app.md | 2 +- docs/api/middleware/healthcheck.md | 105 ++++++++++++++++++ middleware/healthcheck/config.go | 84 +++++++++++++++ middleware/healthcheck/healthcheck.go | 52 +++++++++ middleware/healthcheck/healthcheck_test.go | 117 +++++++++++++++++++++ 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 docs/api/middleware/healthcheck.md create mode 100644 middleware/healthcheck/config.go create mode 100644 middleware/healthcheck/healthcheck.go create mode 100644 middleware/healthcheck/healthcheck_test.go diff --git a/docs/api/app.md b/docs/api/app.md index 198c83ce1d..cb38f7051f 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -654,4 +654,4 @@ Hooks is a method to return [hooks](../guide/hooks.md) property. ```go title="Signature" func (app *App) Hooks() *Hooks -``` +``` \ No newline at end of file diff --git a/docs/api/middleware/healthcheck.md b/docs/api/middleware/healthcheck.md new file mode 100644 index 0000000000..3f2bb6c5fb --- /dev/null +++ b/docs/api/middleware/healthcheck.md @@ -0,0 +1,105 @@ +--- +id: healthcheck +title: healthcheck +--- + +Liveness and readiness probes middleware for [Fiber](https://github.com/gofiber/fiber) that provides two endpoints for checking the liveness and readiness state of HTTP applications. + +## Overview + +- **Liveness Probe**: Checks if the server is up and running. + - **Default Endpoint**: `/livez` + - **Behavior**: By default returns `true` immediately when the server is operational. + +- **Readiness Probe**: Assesses if the application is ready to handle requests. + - **Default Endpoint**: `/readyz` + - **Behavior**: By default returns `true` immediately when the server is operational. + +- **HTTP Status Codes**: + - `200 OK`: Returned when the checker function evaluates to `true`. + - `503 Service Unavailable`: Returned when the checker function evaluates to `false`. + +## Signatures + +```go +func New(config Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/healthcheck" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Provide a minimal config +app.Use(healthcheck.New()) + +// Or extend your config for customization +app.Use(healthcheck.New(healthcheck.Config{ + LivenessProbe: func(c *fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/live", + ReadinessProbe: func(c *fiber.Ctx) bool { + return serviceA.Ready() && serviceB.Ready() && ... + }, + ReadinessEndpoint: "/ready", +})) +``` + +## Config + +```go +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Function used for checking the liveness of the application. Returns true if the application + // is running and false if it is not. The liveness probe is typically used to indicate if + // the application is in a state where it can handle requests (e.g., the server is up and running). + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + LivenessProbe HealthChecker + + // HTTP endpoint at which the liveness probe will be available. + // + // Optional. Default: "/livez" + LivenessEndpoint string + + // Function used for checking the readiness of the application. Returns true if the application + // is ready to process requests and false otherwise. The readiness probe typically checks if all necessary + // services, databases, and other dependencies are available for the application to function correctly. + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + ReadinessProbe HealthChecker + + // HTTP endpoint at which the readiness probe will be available. + // Optional. Default: "/readyz" + ReadinessEndpoint string +} +``` + +## Default Config + +The default configuration used by this middleware is defined as follows: +```go +func defaultLivenessProbe(*fiber.Ctx) bool { return true } + +func defaultReadinessProbe(*fiber.Ctx) bool { return true } + +var ConfigDefault = Config{ + LivenessProbe: defaultLivenessProbe, + ReadinessProbe: defaultReadinessProbe, + LivenessEndpoint: "/livez", + ReadinessEndpoint: "/readyz", +} +``` \ No newline at end of file diff --git a/middleware/healthcheck/config.go b/middleware/healthcheck/config.go new file mode 100644 index 0000000000..d4e5fac1b0 --- /dev/null +++ b/middleware/healthcheck/config.go @@ -0,0 +1,84 @@ +package healthcheck + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the configuration options for the healthcheck middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Function used for checking the liveness of the application. Returns true if the application + // is running and false if it is not. The liveness probe is typically used to indicate if + // the application is in a state where it can handle requests (e.g., the server is up and running). + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + LivenessProbe HealthChecker + + // HTTP endpoint at which the liveness probe will be available. + // + // Optional. Default: "/livez" + LivenessEndpoint string + + // Function used for checking the readiness of the application. Returns true if the application + // is ready to process requests and false otherwise. The readiness probe typically checks if all necessary + // services, databases, and other dependencies are available for the application to function correctly. + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + ReadinessProbe HealthChecker + + // HTTP endpoint at which the readiness probe will be available. + // Optional. Default: "/readyz" + ReadinessEndpoint string +} + +const ( + DefaultLivenessEndpoint = "/livez" + DefaultReadinessEndpoint = "/readyz" +) + +func defaultLivenessProbe(*fiber.Ctx) bool { return true } + +func defaultReadinessProbe(*fiber.Ctx) bool { return true } + +// ConfigDefault is the default config +var ConfigDefault = Config{ + LivenessProbe: defaultLivenessProbe, + ReadinessProbe: defaultReadinessProbe, + LivenessEndpoint: DefaultLivenessEndpoint, + ReadinessEndpoint: DefaultReadinessEndpoint, +} + +// defaultConfig returns a default config for the healthcheck middleware. +func defaultConfig(config ...Config) Config { + if len(config) < 1 { + return ConfigDefault + } + + cfg := config[0] + + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + if cfg.LivenessProbe == nil { + cfg.LivenessProbe = defaultLivenessProbe + } + + if cfg.ReadinessProbe == nil { + cfg.ReadinessProbe = defaultReadinessProbe + } + + if cfg.LivenessEndpoint == "" { + cfg.LivenessEndpoint = DefaultLivenessEndpoint + } + + if cfg.ReadinessEndpoint == "" { + cfg.ReadinessEndpoint = DefaultReadinessEndpoint + } + + return cfg +} diff --git a/middleware/healthcheck/healthcheck.go b/middleware/healthcheck/healthcheck.go new file mode 100644 index 0000000000..14ff33430c --- /dev/null +++ b/middleware/healthcheck/healthcheck.go @@ -0,0 +1,52 @@ +package healthcheck + +import ( + "github.com/gofiber/fiber/v2" +) + +// HealthChecker defines a function to check liveness or readiness of the application +type HealthChecker func(*fiber.Ctx) bool + +// ProbeCheckerHandler defines a function that returns a ProbeChecker +type HealthCheckerHandler func(HealthChecker) fiber.Handler + +func healthCheckerHandler(checker HealthChecker) fiber.Handler { + return func(c *fiber.Ctx) error { + if checker == nil { + return c.Next() + } + + if checker(c) { + return c.SendStatus(fiber.StatusOK) + } + + return c.SendStatus(fiber.StatusServiceUnavailable) + } +} + +func New(config ...Config) fiber.Handler { + cfg := defaultConfig(config...) + + isLiveHandler := healthCheckerHandler(cfg.LivenessProbe) + isReadyHandler := healthCheckerHandler(cfg.ReadinessProbe) + + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + if c.Method() != fiber.MethodGet { + return c.Next() + } + + switch c.Path() { + case cfg.ReadinessEndpoint: + return isReadyHandler(c) + case cfg.LivenessEndpoint: + return isLiveHandler(c) + } + + return c.Next() + } +} diff --git a/middleware/healthcheck/healthcheck_test.go b/middleware/healthcheck/healthcheck_test.go new file mode 100644 index 0000000000..df0165f158 --- /dev/null +++ b/middleware/healthcheck/healthcheck_test.go @@ -0,0 +1,117 @@ +package healthcheck + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" +) + +func Test_HealthCheck_Default(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New()) + + req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/readyz", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) + + req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/livez", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) +} + +func Test_HealthCheck_Custom(t *testing.T) { + t.Parallel() + + app := fiber.New() + + c1 := make(chan struct{}, 1) + go func() { + time.Sleep(1 * time.Second) + c1 <- struct{}{} + }() + + app.Use(New(Config{ + LivenessProbe: func(c *fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/live", + ReadinessProbe: func(c *fiber.Ctx) bool { + select { + case <-c1: + return true + default: + return false + } + }, + ReadinessEndpoint: "/ready", + })) + + // Live should return 200 with GET request + req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/live", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) + + // Live should return 404 with POST request + req, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/live", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) + + // Ready should return 404 with POST request + req, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/ready", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) + + // Ready should return 503 with GET request before the channel is closed + req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/ready", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusServiceUnavailable, req.StatusCode) + + time.Sleep(1 * time.Second) + + // Ready should return 200 with GET request after the channel is closed + req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/ready", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) +} + +func Test_HealthCheck_Next(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + Next: func(c *fiber.Ctx) bool { + return true + }, + })) + + req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/livez", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) +} + +func Benchmark_HealthCheck(b *testing.B) { + app := fiber.New() + + app.Use(New()) + + h := app.Handler() + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/livez") + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusOK, fctx.Response.Header.StatusCode()) +} From 89f551becc0499931902e38b4249b8f2a4b313c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Fri, 5 Jan 2024 14:02:59 +0100 Subject: [PATCH 18/46] prepare release v2.52.0 - add more Parser tests --- app.go | 2 +- ctx_test.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 47a01dd08a..2ce88704c5 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.51.0" +const Version = "2.52.0" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error diff --git a/ctx_test.go b/ctx_test.go index ea9dfa8485..a278d2f250 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1372,6 +1372,100 @@ func Benchmark_Ctx_Fresh_WithNoCache(b *testing.B) { } } +// go test -run Test_Ctx_Parsers -v +func Test_Ctx_Parsers(t *testing.T) { + t.Parallel() + // setup + app := New() + + type TestStruct struct { + Name string + Class int + NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" reqHeader:"name2"` + ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" reqHeader:"class2"` + } + + withValues := func(t *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) { + t.Helper() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + testStruct := new(TestStruct) + + utils.AssertEqual(t, nil, actionFn(c, testStruct)) + utils.AssertEqual(t, "foo", testStruct.Name) + utils.AssertEqual(t, 111, testStruct.Class) + utils.AssertEqual(t, "bar", testStruct.NameWithDefault) + utils.AssertEqual(t, 222, testStruct.ClassWithDefault) + } + + t.Run("BodyParser:xml", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().SetBody([]byte(`foo111bar222`)) + return c.BodyParser(testStruct) + }) + }) + t.Run("BodyParser:form", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte(`name=foo&class=111&name2=bar&class2=222`)) + return c.BodyParser(testStruct) + }) + }) + t.Run("BodyParser:json", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().SetBody([]byte(`{"name":"foo","class":111,"name2":"bar","class2":222}`)) + return c.BodyParser(testStruct) + }) + }) + t.Run("BodyParser:multiform", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"class\"\r\n\r\n111\r\n--b\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"class2\"\r\n\r\n222\r\n--b--") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Request().Header.SetContentLength(len(body)) + return c.BodyParser(testStruct) + }) + }) + t.Run("CookieParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.Set("Cookie", "name=foo;name2=bar;class=111;class2=222") + return c.CookieParser(testStruct) + }) + }) + t.Run("QueryParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().URI().SetQueryString("name=foo&name2=bar&class=111&class2=222") + return c.QueryParser(testStruct) + }) + }) + t.Run("ParamsParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.route = &Route{Params: []string{"name", "name2", "class", "class2"}} + c.values = [30]string{"foo", "bar", "111", "222"} + return c.ParamsParser(testStruct) + }) + }) + t.Run("ReqHeaderParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.Add("name", "foo") + c.Request().Header.Add("name2", "bar") + c.Request().Header.Add("class", "111") + c.Request().Header.Add("class2", "222") + return c.ReqHeaderParser(testStruct) + }) + }) +} + // go test -run Test_Ctx_Get func Test_Ctx_Get(t *testing.T) { t.Parallel() From 476e1ed9fe3e0338e37c7ba36901e36bea1f298f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Fri, 5 Jan 2024 14:36:18 +0100 Subject: [PATCH 19/46] fix healthcheck.md --- docs/api/middleware/healthcheck.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/api/middleware/healthcheck.md b/docs/api/middleware/healthcheck.md index 3f2bb6c5fb..641c236b1b 100644 --- a/docs/api/middleware/healthcheck.md +++ b/docs/api/middleware/healthcheck.md @@ -1,8 +1,9 @@ --- id: healthcheck -title: healthcheck --- +# Health Check + Liveness and readiness probes middleware for [Fiber](https://github.com/gofiber/fiber) that provides two endpoints for checking the liveness and readiness state of HTTP applications. ## Overview @@ -27,7 +28,7 @@ func New(config Config) fiber.Handler ## Examples -Import the middleware package that is part of the Fiber web framework +Import the middleware package that is part of the [Fiber](https://github.com/gofiber/fiber) web framework ```go import ( "github.com/gofiber/fiber/v2" @@ -35,7 +36,7 @@ import ( ) ``` -After you initiate your Fiber app, you can use the following possibilities: +After you initiate your [Fiber](https://github.com/gofiber/fiber) app, you can use the following possibilities: ```go // Provide a minimal config @@ -102,4 +103,4 @@ var ConfigDefault = Config{ LivenessEndpoint: "/livez", ReadinessEndpoint: "/readyz", } -``` \ No newline at end of file +``` From 2e66937b4e9f2d313dbbc3c0f5c21f28fc60119f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Sat, 6 Jan 2024 17:36:42 +0100 Subject: [PATCH 20/46] configure workflows for V2 branch --- .../ISSUE_TEMPLATE/feature-request-v3.yaml | 74 ------------------- .../ISSUE_TEMPLATE/maintenance-task-v3.yaml | 54 -------------- .github/PULL_REQUEST_TEMPLATE/v3-changes.md | 44 ----------- .github/pull_request_template.md | 2 - .github/release-drafter.yml | 2 + .github/scripts/sync_docs.sh | 19 ++--- .github/workflows/benchmark.yml | 3 +- .github/workflows/codeql-analysis.yml | 3 +- .github/workflows/linter.yml | 3 +- .github/workflows/release-drafter.yml | 3 +- .github/workflows/sync-docs.yml | 11 ++- .github/workflows/test.yml | 3 +- .github/workflows/vulncheck.yml | 3 +- 13 files changed, 23 insertions(+), 201 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature-request-v3.yaml delete mode 100644 .github/ISSUE_TEMPLATE/maintenance-task-v3.yaml delete mode 100644 .github/PULL_REQUEST_TEMPLATE/v3-changes.md diff --git a/.github/ISSUE_TEMPLATE/feature-request-v3.yaml b/.github/ISSUE_TEMPLATE/feature-request-v3.yaml deleted file mode 100644 index c7551b6d0e..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request-v3.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: "📝 Feature Proposal for v3" -title: "📝 [v3 Proposal]: " -description: Propose a feature or improvement for Fiber v3. -labels: ["📝 Proposal", "v3"] - -body: - - type: markdown - id: notice - attributes: - value: | - ### Notice - - For questions, join our [Discord server](https://gofiber.io/discord). - - Please write in clear, understandable English. - - Ensure your proposal aligns with Express design principles and HTTP RFC standards. - - Describe features expected to remain stable and not require changes in the foreseeable future. - - - type: textarea - id: description - attributes: - label: "Feature Proposal Description" - description: "A clear and detailed description of the feature you are proposing for Fiber v3. How should it work, and what API endpoints and methods would it involve?" - placeholder: "Describe your feature proposal clearly and in detail, including API endpoints and methods." - validations: - required: true - - - type: textarea - id: express-alignment - attributes: - label: "Alignment with Express API" - description: "Explain how your proposal aligns with the design and API of Express.js. Provide comparative examples if possible." - placeholder: "Outline how the feature aligns with Express.js design principles and API standards." - validations: - required: true - - - type: textarea - id: standards-compliance - attributes: - label: "HTTP RFC Standards Compliance" - description: "Confirm that the feature complies with HTTP RFC standards, and describe any relevant aspects." - placeholder: "Detail how the feature adheres to HTTP RFC standards." - validations: - required: true - - - type: textarea - id: stability - attributes: - label: "API Stability" - description: "Discuss the expected stability of the feature and its API. How do you ensure that it will not require changes or deprecations in the near future?" - placeholder: "Describe measures taken to ensure the feature's API stability over time." - validations: - required: true - - - type: textarea - id: examples - attributes: - label: "Feature Examples" - description: "Provide concrete examples and code snippets to illustrate how the proposed feature should function." - placeholder: "Share code snippets that exemplify the proposed feature and its usage." - render: go - validations: - required: true - - - type: checkboxes - id: terms - attributes: - label: "Checklist:" - description: "By submitting this issue, you confirm that:" - options: - - label: "I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md)." - required: true - - label: "I have searched for existing issues that describe my proposal before opening this one." - required: true - - label: "I understand that a proposal that does not meet these guidelines may be closed without explanation." - required: true diff --git a/.github/ISSUE_TEMPLATE/maintenance-task-v3.yaml b/.github/ISSUE_TEMPLATE/maintenance-task-v3.yaml deleted file mode 100644 index 2a58f78922..0000000000 --- a/.github/ISSUE_TEMPLATE/maintenance-task-v3.yaml +++ /dev/null @@ -1,54 +0,0 @@ -name: "🧹 v3 Maintenance Task" -title: "🧹 [v3 Maintenance]: " -description: Describe a maintenance task for the v3 of the Fiber project. -labels: ["🧹 Updates", "v3"] - -body: - - type: markdown - id: notice - attributes: - value: | - ### Notice - - Before submitting a maintenance task, please check if a similar task has already been filed. - - Clearly outline the purpose of the maintenance task and its impact on the project. - - Use clear and understandable English. - - - type: textarea - id: task-description - attributes: - label: "Maintenance Task Description" - description: "Provide a detailed description of the maintenance task. Include any specific areas of the codebase that require attention, and the desired outcomes of this task." - placeholder: "Detail the maintenance task, specifying what needs to be done and why it is necessary." - validations: - required: true - - - type: textarea - id: impact - attributes: - label: "Impact on the Project" - description: "Explain the impact this maintenance will have on the project. Include benefits and potential risks if applicable." - placeholder: "Describe how completing this task will benefit the project, or the risks of not addressing it." - validations: - required: false - - - type: textarea - id: additional-context - attributes: - label: "Additional Context (optional)" - description: "Any additional information or context regarding the maintenance task that might be helpful." - placeholder: "Provide any additional information that may be relevant to the task at hand." - validations: - required: false - - - type: checkboxes - id: terms - attributes: - label: "Checklist:" - description: "Please confirm the following:" - options: - - label: "I have confirmed that this maintenance task is currently not being addressed." - required: true - - label: "I understand that this task will be evaluated by the maintainers and prioritized accordingly." - required: true - - label: "I am available to provide further information if needed." - required: true diff --git a/.github/PULL_REQUEST_TEMPLATE/v3-changes.md b/.github/PULL_REQUEST_TEMPLATE/v3-changes.md deleted file mode 100644 index 250037be08..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE/v3-changes.md +++ /dev/null @@ -1,44 +0,0 @@ -## Description - -Please provide a clear and concise description of the changes you've made and the problem they address. Include the purpose of the change, any relevant issues it solves, and the benefits it brings to the project. If this change introduces new features or adjustments, highlight them here. - -Related to # (issue) - -## Changes Introduced - -List the new features or adjustments introduced in this pull request. Provide details on benchmarks, documentation updates, changelog entries, and if applicable, the migration guide. - -- [ ] Benchmarks: Describe any performance benchmarks and improvements related to the changes. -- [ ] Documentation Update: Detail the updates made to the documentation and links to the changed files. -- [ ] Changelog/What's New: Include a summary of the additions for the upcoming release notes. -- [ ] Migration Guide: If necessary, provide a guide or steps for users to migrate their existing code to accommodate these changes. -- [ ] API Alignment with Express: Explain how the changes align with the Express API. -- [ ] API Longevity: Discuss the steps taken to ensure that the new or updated APIs are consistent and not prone to breaking changes. -- [ ] Examples: Provide examples demonstrating the new features or changes in action. - -## Type of Change - -Please delete options that are not relevant. - -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Enhancement (improvement to existing features and functionality) -- [ ] Documentation update (changes to documentation) -- [ ] Performance improvement (non-breaking change which improves efficiency) -- [ ] Code consistency (non-breaking change which improves code reliability and robustness) - -## Checklist - -Before you submit your pull request, please make sure you meet these requirements: - -- [ ] Followed the inspiration of the Express.js framework for new functionalities, making them similar in usage. -- [ ] Conducted a self-review of the code and provided comments for complex or critical parts. -- [ ] Updated the documentation in the `/docs/` directory for [Fiber's documentation](https://docs.gofiber.io/). -- [ ] Added or updated unit tests to validate the effectiveness of the changes or new features. -- [ ] Ensured that new and existing unit tests pass locally with the changes. -- [ ] Verified that any new dependencies are essential and have been agreed upon by the maintainers/community. -- [ ] Aimed for optimal performance with minimal allocations in the new code. -- [ ] Provided benchmarks for the new code to analyze and improve upon. - -## Commit Formatting - -Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: [CONTRIBUTING.md](https://github.com/gofiber/fiber/blob/master/.github/CONTRIBUTING.md#pull-requests-or-commits) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 323c265e79..222b26d20b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,8 +5,6 @@ Explain the *details* for making this change. What existing problem does the pul Fixes # (issue) -:warning: **For changes specific to v3, please switch to the [v3 Pull Request Template](?template=v3-changes.md).** - ## Type of change Please delete options that are not relevant. diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index ebc7c6f96c..5c8b72d1d6 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,5 +1,7 @@ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' +commitish: refs/heads/v2 +filter-by-commitish: true categories: - title: '❗ Breaking Changes' labels: diff --git a/.github/scripts/sync_docs.sh b/.github/scripts/sync_docs.sh index b9b4cefde1..8175f26d21 100755 --- a/.github/scripts/sync_docs.sh +++ b/.github/scripts/sync_docs.sh @@ -18,15 +18,16 @@ git config --global user.name "${AUTHOR_USERNAME}" git clone https://${TOKEN}@${REPO_URL} fiber-docs # Handle push event -if [ "$EVENT" == "push" ]; then - latest_commit=$(git rev-parse --short HEAD) - log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/) - if [[ $log_output != "" ]]; then - cp -a docs/* fiber-docs/docs/${REPO_DIR} - fi - -# Handle release event -elif [ "$EVENT" == "release" ]; then +#if [ "$EVENT" == "push" ]; then +# latest_commit=$(git rev-parse --short HEAD) +# log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/) +# if [[ $log_output != "" ]]; then +# cp -a docs/* fiber-docs/docs/${REPO_DIR} +# fi +# +## Handle release event +#el +if [ "$EVENT" == "release" ]; then major_version="${TAG_NAME%%.*}" # Form new version name diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 7625373255..42e7afd653 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,8 +1,7 @@ on: push: branches: - - master - - main + - v2 paths: - "**" - "!docs/**" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 82036cc967..0476abfba2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,8 +3,7 @@ name: "CodeQL" on: push: branches: - - master - - main + - v2 paths: - "**" - "!docs/**" diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 3defb96344..607e0fa055 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -4,8 +4,7 @@ name: golangci-lint on: push: branches: - - master - - main + - v2 pull_request: permissions: contents: read diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index cedaab432f..3ac6e95572 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -4,8 +4,7 @@ on: push: # branches to consider in the event; optional, defaults to all branches: - - master - - main + - v2 jobs: update_release_draft: diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index eb35d75bd2..dd53569b2e 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -1,12 +1,11 @@ name: "Sync docs" on: - push: - branches: - - master - - main - paths: - - "docs/**" +# push: +# branches: +# - v2 +# paths: +# - "docs/**" release: types: [published] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f103d914fb..a186908aa0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,7 @@ on: push: branches: - - master - - main + - v2 paths: - "**" - "!docs/**" diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index e6d29ccb4b..f28ab5b0e2 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -3,8 +3,7 @@ name: Run govulncheck on: push: branches: - - master - - main + - v2 paths: - "**" - "!docs/**" From 7ba3137f2e6fd57c7dc198eaf5de77deb20646e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Sat, 6 Jan 2024 17:45:21 +0100 Subject: [PATCH 21/46] configure workflows for V2 branch --- .github/release-drafter.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 5c8b72d1d6..d008535545 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -2,6 +2,10 @@ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' commitish: refs/heads/v2 filter-by-commitish: true +include-labels: + - 'v2' +exclude-labels: + - 'v3' categories: - title: '❗ Breaking Changes' labels: From e524b7352468da1b788c3cdbd680ff7d1055b2f1 Mon Sep 17 00:00:00 2001 From: Jongmin Kim Date: Mon, 29 Jan 2024 02:28:47 +0900 Subject: [PATCH 22/46] Fix default value to false in docs of QueryBool (#2811) fix default value to false in docs of QueryBool --- ctx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctx.go b/ctx.go index 55b81cf2ee..596268e9fe 100644 --- a/ctx.go +++ b/ctx.go @@ -1222,7 +1222,7 @@ func (c *Ctx) QueryInt(key string, defaultValue ...int) int { } // QueryBool returns bool value of key string parameter in the url. -// Default to empty or invalid key is true. +// Default to empty or invalid key is false. // // Get /?name=alex&want_pizza=false&id= // QueryBool("want_pizza") == false From 8325ed086c99920f63d9b916644774e4e295eab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Tue, 6 Feb 2024 08:40:32 +0100 Subject: [PATCH 23/46] update queryParser config --- docs/api/ctx.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 2c3244926f..f1121b7630 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -1387,9 +1387,13 @@ app.Get("/", func(c *fiber.Ctx) error { return err } - log.Println(p.Name) // john - log.Println(p.Pass) // doe - log.Println(p.Products) // [shoe, hat] + log.Println(p.Name) // john + log.Println(p.Pass) // doe + // fiber.Config{EnableSplittingOnParsers: false} - default + log.Println(p.Products) // ["shoe,hat"] + // fiber.Config{EnableSplittingOnParsers: true} + // log.Println(p.Products) // ["shoe", "hat"] + // ... }) @@ -1398,6 +1402,10 @@ app.Get("/", func(c *fiber.Ctx) error { // curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" ``` +:::info +For more parser settings please look here [Config](fiber.md#Config) +::: + ## Range A struct containing the type and a slice of ranges will be returned. From 56d2ec7bd0d796399fcca2d4668dfc34c9ef858f Mon Sep 17 00:00:00 2001 From: RW Date: Tue, 6 Feb 2024 09:11:21 +0100 Subject: [PATCH 24/46] Update ctx.md --- docs/api/ctx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/ctx.md b/docs/api/ctx.md index f1121b7630..52f1a3ad91 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -1403,7 +1403,7 @@ app.Get("/", func(c *fiber.Ctx) error { ``` :::info -For more parser settings please look here [Config](fiber.md#Config) +For more parser settings please look here [Config](fiber.md#config) ::: ## Range From 4e0f180fe3425b92d2c7b7e362182d17c21ee50b Mon Sep 17 00:00:00 2001 From: RW Date: Thu, 8 Feb 2024 08:18:26 +0100 Subject: [PATCH 25/46] Update routing.md --- docs/guide/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/routing.md b/docs/guide/routing.md index b6534532b0..2a1067ae6f 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -145,7 +145,7 @@ We have adapted the routing strongly to the express routing, but currently witho Route constraints execute when a match has occurred to the incoming URL and the URL path is tokenized into route values by parameters. The feature was intorduced in `v2.37.0` and inspired by [.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-6.0#route-constraints). :::caution -Constraints aren't validation for parameters. If constraint aren't valid for parameter value, Fiber returns **404 handler**. +Constraints aren't validation for parameters. If constraints aren't valid for a parameter value, Fiber returns **404 handler**. ::: | Constraint | Example | Example matches | From a84a7cee7edcfb12c6448a0b408d3f804c38743f Mon Sep 17 00:00:00 2001 From: Giovanni Rivera Date: Sun, 18 Feb 2024 18:21:23 -0800 Subject: [PATCH 26/46] :books: Doc: Fix code snippet indentation in /docs/api/middleware/keyauth.md Removes an an extra level of indentation in line 51 of `keyauth.md` [here](https://github.com/gofiber/fiber/blob/v2/docs/api/middleware/keyauth.md?plain=1#L51) --- docs/api/middleware/keyauth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/middleware/keyauth.md b/docs/api/middleware/keyauth.md index 1a719c134d..4705c2e667 100644 --- a/docs/api/middleware/keyauth.md +++ b/docs/api/middleware/keyauth.md @@ -47,7 +47,7 @@ func main() { Validator: validateAPIKey, })) - app.Get("/", func(c *fiber.Ctx) error { + app.Get("/", func(c *fiber.Ctx) error { return c.SendString("Successfully authenticated!") }) From 5e30112d08b1a76f38f838a175988a3712846bd7 Mon Sep 17 00:00:00 2001 From: Lucas Lemos Date: Mon, 19 Feb 2024 10:28:58 -0300 Subject: [PATCH 27/46] fix: healthcheck middleware not working with route group (#2863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: healthcheck middleware not working with route group * perf: change verification method to improve perf * Update healthcheck_test.go * test: add not matching route test for strict routing * add more test cases * correct tests * correct test helpers * correct tests * correct tests --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: RenÊ Werner --- middleware/healthcheck/healthcheck.go | 19 ++- middleware/healthcheck/healthcheck_test.go | 178 +++++++++++++++++---- 2 files changed, 163 insertions(+), 34 deletions(-) diff --git a/middleware/healthcheck/healthcheck.go b/middleware/healthcheck/healthcheck.go index 14ff33430c..c9d6a6476b 100644 --- a/middleware/healthcheck/healthcheck.go +++ b/middleware/healthcheck/healthcheck.go @@ -2,6 +2,7 @@ package healthcheck import ( "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" ) // HealthChecker defines a function to check liveness or readiness of the application @@ -40,11 +41,19 @@ func New(config ...Config) fiber.Handler { return c.Next() } - switch c.Path() { - case cfg.ReadinessEndpoint: - return isReadyHandler(c) - case cfg.LivenessEndpoint: - return isLiveHandler(c) + prefixCount := len(utils.TrimRight(c.Route().Path, '/')) + if len(c.Path()) >= prefixCount { + checkPath := c.Path()[prefixCount:] + checkPathTrimmed := checkPath + if !c.App().Config().StrictRouting { + checkPathTrimmed = utils.TrimRight(checkPath, '/') + } + switch { + case checkPath == cfg.ReadinessEndpoint || checkPathTrimmed == cfg.ReadinessEndpoint: + return isReadyHandler(c) + case checkPath == cfg.LivenessEndpoint || checkPathTrimmed == cfg.LivenessEndpoint: + return isLiveHandler(c) + } } return c.Next() diff --git a/middleware/healthcheck/healthcheck_test.go b/middleware/healthcheck/healthcheck_test.go index df0165f158..84fbb43da0 100644 --- a/middleware/healthcheck/healthcheck_test.go +++ b/middleware/healthcheck/healthcheck_test.go @@ -1,28 +1,120 @@ package healthcheck import ( + "fmt" "net/http/httptest" "testing" - "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/utils" "github.com/valyala/fasthttp" ) +func shouldGiveStatus(t *testing.T, app *fiber.App, path string, expectedStatus int) { + t.Helper() + req, err := app.Test(httptest.NewRequest(fiber.MethodGet, path, nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expectedStatus, req.StatusCode, "path: "+path+" should match "+fmt.Sprint(expectedStatus)) +} + +func shouldGiveOK(t *testing.T, app *fiber.App, path string) { + t.Helper() + shouldGiveStatus(t, app, path, fiber.StatusOK) +} + +func shouldGiveNotFound(t *testing.T, app *fiber.App, path string) { + t.Helper() + shouldGiveStatus(t, app, path, fiber.StatusNotFound) +} + +func Test_HealthCheck_Strict_Routing_Default(t *testing.T) { + t.Parallel() + + app := fiber.New(fiber.Config{ + StrictRouting: true, + }) + + app.Use(New()) + + shouldGiveOK(t, app, "/readyz") + shouldGiveOK(t, app, "/livez") + shouldGiveNotFound(t, app, "/readyz/") + shouldGiveNotFound(t, app, "/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") +} + +func Test_HealthCheck_Group_Default(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Group("/v1", New()) + v2Group := app.Group("/v2/") + customer := v2Group.Group("/customer/") + customer.Use(New()) + + v3Group := app.Group("/v3/") + v3Group.Group("/todos/", New(Config{ReadinessEndpoint: "/readyz/", LivenessEndpoint: "/livez/"})) + + shouldGiveOK(t, app, "/v1/readyz") + shouldGiveOK(t, app, "/v1/livez") + shouldGiveOK(t, app, "/v1/readyz/") + shouldGiveOK(t, app, "/v1/livez/") + shouldGiveOK(t, app, "/v2/customer/readyz") + shouldGiveOK(t, app, "/v2/customer/livez") + shouldGiveOK(t, app, "/v2/customer/readyz/") + shouldGiveOK(t, app, "/v2/customer/livez/") + shouldGiveNotFound(t, app, "/v3/todos/readyz") + shouldGiveNotFound(t, app, "/v3/todos/livez") + shouldGiveOK(t, app, "/v3/todos/readyz/") + shouldGiveOK(t, app, "/v3/todos/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") + shouldGiveNotFound(t, app, "/notDefined/readyz/") + shouldGiveNotFound(t, app, "/notDefined/livez/") + + // strict routing + app = fiber.New(fiber.Config{ + StrictRouting: true, + }) + app.Group("/v1", New()) + v2Group = app.Group("/v2/") + customer = v2Group.Group("/customer/") + customer.Use(New()) + + v3Group = app.Group("/v3/") + v3Group.Group("/todos/", New(Config{ReadinessEndpoint: "/readyz/", LivenessEndpoint: "/livez/"})) + + shouldGiveOK(t, app, "/v1/readyz") + shouldGiveOK(t, app, "/v1/livez") + shouldGiveNotFound(t, app, "/v1/readyz/") + shouldGiveNotFound(t, app, "/v1/livez/") + shouldGiveOK(t, app, "/v2/customer/readyz") + shouldGiveOK(t, app, "/v2/customer/livez") + shouldGiveNotFound(t, app, "/v2/customer/readyz/") + shouldGiveNotFound(t, app, "/v2/customer/livez/") + shouldGiveNotFound(t, app, "/v3/todos/readyz") + shouldGiveNotFound(t, app, "/v3/todos/livez") + shouldGiveOK(t, app, "/v3/todos/readyz/") + shouldGiveOK(t, app, "/v3/todos/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") + shouldGiveNotFound(t, app, "/notDefined/readyz/") + shouldGiveNotFound(t, app, "/notDefined/livez/") +} + func Test_HealthCheck_Default(t *testing.T) { t.Parallel() app := fiber.New() app.Use(New()) - req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/readyz", nil)) - utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) - - req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/livez", nil)) - utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) + shouldGiveOK(t, app, "/readyz") + shouldGiveOK(t, app, "/livez") + shouldGiveOK(t, app, "/readyz/") + shouldGiveOK(t, app, "/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") } func Test_HealthCheck_Custom(t *testing.T) { @@ -31,11 +123,6 @@ func Test_HealthCheck_Custom(t *testing.T) { app := fiber.New() c1 := make(chan struct{}, 1) - go func() { - time.Sleep(1 * time.Second) - c1 <- struct{}{} - }() - app.Use(New(Config{ LivenessProbe: func(c *fiber.Ctx) bool { return true @@ -53,12 +140,9 @@ func Test_HealthCheck_Custom(t *testing.T) { })) // Live should return 200 with GET request - req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/live", nil)) - utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) - + shouldGiveOK(t, app, "/live") // Live should return 404 with POST request - req, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/live", nil)) + req, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/live", nil)) utils.AssertEqual(t, nil, err) utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) @@ -68,16 +152,53 @@ func Test_HealthCheck_Custom(t *testing.T) { utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) // Ready should return 503 with GET request before the channel is closed - req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/ready", nil)) - utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, fiber.StatusServiceUnavailable, req.StatusCode) - - time.Sleep(1 * time.Second) + shouldGiveStatus(t, app, "/ready", fiber.StatusServiceUnavailable) // Ready should return 200 with GET request after the channel is closed - req, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/ready", nil)) - utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, fiber.StatusOK, req.StatusCode) + c1 <- struct{}{} + shouldGiveOK(t, app, "/ready") +} + +func Test_HealthCheck_Custom_Nested(t *testing.T) { + t.Parallel() + + app := fiber.New() + + c1 := make(chan struct{}, 1) + + app.Use(New(Config{ + LivenessProbe: func(c *fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/probe/live", + ReadinessProbe: func(c *fiber.Ctx) bool { + select { + case <-c1: + return true + default: + return false + } + }, + ReadinessEndpoint: "/probe/ready", + })) + + shouldGiveOK(t, app, "/probe/live") + shouldGiveStatus(t, app, "/probe/ready", fiber.StatusServiceUnavailable) + shouldGiveOK(t, app, "/probe/live/") + shouldGiveStatus(t, app, "/probe/ready/", fiber.StatusServiceUnavailable) + shouldGiveNotFound(t, app, "/probe/livez") + shouldGiveNotFound(t, app, "/probe/readyz") + shouldGiveNotFound(t, app, "/probe/livez/") + shouldGiveNotFound(t, app, "/probe/readyz/") + shouldGiveNotFound(t, app, "/livez") + shouldGiveNotFound(t, app, "/readyz") + shouldGiveNotFound(t, app, "/readyz/") + shouldGiveNotFound(t, app, "/livez/") + + c1 <- struct{}{} + shouldGiveOK(t, app, "/probe/ready") + c1 <- struct{}{} + shouldGiveOK(t, app, "/probe/ready/") } func Test_HealthCheck_Next(t *testing.T) { @@ -91,9 +212,8 @@ func Test_HealthCheck_Next(t *testing.T) { }, })) - req, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/livez", nil)) - utils.AssertEqual(t, nil, err) - utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) + shouldGiveNotFound(t, app, "/readyz") + shouldGiveNotFound(t, app, "/livez") } func Benchmark_HealthCheck(b *testing.B) { From f0cd3b44b086544a37886232d0530601f2406c23 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:47:33 -0500 Subject: [PATCH 28/46] Merge pull request from GHSA-fmg4-x8pw-hjhg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enforce Wildcard Origins with AllowCredentials check * Expand unit-tests, fix issues with subdomains logic, update docs * Update cors.md * Added test using localhost, ipv4, and ipv6 address * improve documentation markdown --------- Co-authored-by: RenÊ Werner --- docs/api/middleware/cors.md | 31 ++++--- middleware/cors/cors.go | 61 ++++++++++---- middleware/cors/cors_test.go | 154 ++++++++++++++++++++++++++++------ middleware/cors/utils.go | 99 ++++++++++++++-------- middleware/cors/utils_test.go | 145 ++++++++++++++++++++++++++++++++ 5 files changed, 402 insertions(+), 88 deletions(-) create mode 100644 middleware/cors/utils_test.go diff --git a/docs/api/middleware/cors.md b/docs/api/middleware/cors.md index 9a28342fd3..ca250833d6 100644 --- a/docs/api/middleware/cors.md +++ b/docs/api/middleware/cors.md @@ -10,6 +10,8 @@ The middleware conforms to the `access-control-allow-origin` specification by pa For more control, `AllowOriginsFunc` can be used to programatically determine if an origin is allowed. If no match was found in `AllowOrigins` and if `AllowOriginsFunc` returns true then the 'access-control-allow-origin' response header is set to the 'origin' request header. +When defining your Origins make sure they are properly formatted. The middleware validates and normalizes the provided origins, ensuring they're in the correct format by checking for valid schemes (http or https), and removing any trailing slashes. + ## Signatures ```go @@ -56,18 +58,27 @@ app.Use(cors.New(cors.Config{ })) ``` +**Note: The following configuration is considered insecure and will result in a panic.** + +```go +app.Use(cors.New(cors.Config{ + AllowOrigins: "*", + AllowCredentials: true, +})) +``` + ## Config -| Property | Type | Description | Default | -|:-----------------|:---------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------| -| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| AllowOriginsFunc | `func(origin string) bool` | AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' response header to the 'origin' request header when returned true. | `nil` | -| AllowOrigins | `string` | AllowOrigin defines a comma separated list of origins that may access the resource. | `"*"` | -| AllowMethods | `string` | AllowMethods defines a list of methods allowed when accessing the resource. This is used in response to a preflight request. | `"GET,POST,HEAD,PUT,DELETE,PATCH"` | -| AllowHeaders | `string` | AllowHeaders defines a list of request headers that can be used when making the actual request. This is in response to a preflight request. | `""` | -| AllowCredentials | `bool` | AllowCredentials indicates whether or not the response to the request can be exposed when the credentials flag is true. | `false` | -| ExposeHeaders | `string` | ExposeHeaders defines a whitelist headers that clients are allowed to access. | `""` | -| MaxAge | `int` | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. If you pass MaxAge 0, Access-Control-Max-Age header will not be added and browser will use 5 seconds by default. To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header 0. | `0` | +| Property | Type | Description | Default | +|:-----------------|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| AllowOriginsFunc | `func(origin string) bool` | AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' response header to the 'origin' request header when returned true. This allows for dynamic evaluation of allowed origins. Note if AllowCredentials is true, wildcard origins will be not have the 'access-control-allow-credentials' header set to 'true'. | `nil` | +| AllowOrigins | `string` | AllowOrigin defines a comma separated list of origins that may access the resource. | `"*"` | +| AllowMethods | `string` | AllowMethods defines a list of methods allowed when accessing the resource. This is used in response to a preflight request. | `"GET,POST,HEAD,PUT,DELETE,PATCH"` | +| AllowHeaders | `string` | AllowHeaders defines a list of request headers that can be used when making the actual request. This is in response to a preflight request. | `""` | +| AllowCredentials | `bool` | AllowCredentials indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials. Note: If true, AllowOrigins cannot be set to a wildcard ("*") to prevent security vulnerabilities. | `false` | +| ExposeHeaders | `string` | ExposeHeaders defines a whitelist headers that clients are allowed to access. | `""` | +| MaxAge | `int` | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. If you pass MaxAge 0, Access-Control-Max-Age header will not be added and browser will use 5 seconds by default. To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header 0. | `0` | ## Default Config diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index ebc1c6b1cb..2ca3767d1f 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -16,12 +16,14 @@ type Config struct { Next func(c *fiber.Ctx) bool // AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' - // response header to the 'origin' request header when returned true. + // response header to the 'origin' request header when returned true. This allows for + // dynamic evaluation of allowed origins. Note if AllowCredentials is true, wildcard origins + // will be not have the 'access-control-allow-credentials' header set to 'true'. // // Optional. Default: nil AllowOriginsFunc func(origin string) bool - // AllowOrigin defines a list of origins that may access the resource. + // AllowOrigin defines a comma separated list of origins that may access the resource. // // Optional. Default value "*" AllowOrigins string @@ -41,7 +43,8 @@ type Config struct { // AllowCredentials indicates whether or not the response to the request // can be exposed when the credentials flag is true. When used as part of // a response to a preflight request, this indicates whether or not the - // actual request can be made using credentials. + // actual request can be made using credentials. Note: If true, AllowOrigins + // cannot be set to a wildcard ("*") to prevent security vulnerabilities. // // Optional. Default value false. AllowCredentials bool @@ -105,6 +108,26 @@ func New(config ...Config) fiber.Handler { log.Warn("[CORS] Both 'AllowOrigins' and 'AllowOriginsFunc' have been defined.") } + // Validate CORS credentials configuration + if cfg.AllowCredentials && cfg.AllowOrigins == "*" { + panic("[CORS] Insecure setup, 'AllowCredentials' is set to true, and 'AllowOrigins' is set to a wildcard.") + } + + // Validate and normalize static AllowOrigins if not using AllowOriginsFunc + if cfg.AllowOriginsFunc == nil && cfg.AllowOrigins != "" && cfg.AllowOrigins != "*" { + validatedOrigins := []string{} + for _, origin := range strings.Split(cfg.AllowOrigins, ",") { + isValid, normalizedOrigin := normalizeOrigin(origin) + if isValid { + validatedOrigins = append(validatedOrigins, normalizedOrigin) + } else { + log.Warnf("[CORS] Invalid origin format in configuration: %s", origin) + panic("[CORS] Invalid origin provided in configuration") + } + } + cfg.AllowOrigins = strings.Join(validatedOrigins, ",") + } + // Convert string to slice allowOrigins := strings.Split(strings.ReplaceAll(cfg.AllowOrigins, " ", ""), ",") @@ -123,22 +146,18 @@ func New(config ...Config) fiber.Handler { return c.Next() } - // Get origin header - origin := c.Get(fiber.HeaderOrigin) + // Get originHeader header + originHeader := c.Get(fiber.HeaderOrigin) allowOrigin := "" // Check allowed origins - for _, o := range allowOrigins { - if o == "*" { + for _, origin := range allowOrigins { + if origin == "*" { allowOrigin = "*" break } - if o == origin { - allowOrigin = o - break - } - if matchSubdomain(origin, o) { - allowOrigin = origin + if validateDomain(originHeader, origin) { + allowOrigin = originHeader break } } @@ -147,8 +166,8 @@ func New(config ...Config) fiber.Handler { // handling the value in 'AllowOrigins' does // not result in allowOrigin being set. if allowOrigin == "" && cfg.AllowOriginsFunc != nil { - if cfg.AllowOriginsFunc(origin) { - allowOrigin = origin + if cfg.AllowOriginsFunc(originHeader) { + allowOrigin = originHeader } } @@ -173,9 +192,17 @@ func New(config ...Config) fiber.Handler { c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) c.Set(fiber.HeaderAccessControlAllowMethods, allowMethods) - // Set Allow-Credentials if set to true if cfg.AllowCredentials { - c.Set(fiber.HeaderAccessControlAllowCredentials, "true") + // When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*' + if allowOrigin != "*" && allowOrigin != "" { + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + c.Set(fiber.HeaderAccessControlAllowCredentials, "true") + } else if allowOrigin == "*" { + log.Warn("[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.") + } + } else { + // For non-credential requests, it's safe to set to '*' or specific origins + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) } // Set Allow-Headers if not empty diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index 22fef8442b..9fc2852556 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -35,7 +35,7 @@ func Test_CORS_Negative_MaxAge(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodOptions) - ctx.Request.Header.Set(fiber.HeaderOrigin, "localhost") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") app.Handler()(ctx) utils.AssertEqual(t, "0", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) @@ -72,7 +72,46 @@ func Test_CORS_Wildcard(t *testing.T) { app := fiber.New() // OPTIONS (preflight) response headers when AllowOrigins is * app.Use(New(Config{ - AllowOrigins: "*", + AllowOrigins: "*", + MaxAge: 3600, + ExposeHeaders: "X-Request-ID", + AllowHeaders: "Authentication", + })) + // Get handler pointer + handler := app.Handler() + + // Make request + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + + // Perform request + handler(ctx) + + // Check result + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) // Validates request is not reflecting origin in the response + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "3600", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) + utils.AssertEqual(t, "Authentication", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) + + // Test non OPTIONS (preflight) response headers + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + handler(ctx) + + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "X-Request-ID", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders))) +} + +// go test -run -v Test_CORS_Origin_AllowCredentials +func Test_CORS_Origin_AllowCredentials(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + // OPTIONS (preflight) response headers when AllowOrigins is * + app.Use(New(Config{ + AllowOrigins: "http://localhost", AllowCredentials: true, MaxAge: 3600, ExposeHeaders: "X-Request-ID", @@ -84,14 +123,14 @@ func Test_CORS_Wildcard(t *testing.T) { // Make request ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") - ctx.Request.Header.Set(fiber.HeaderOrigin, "localhost") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") ctx.Request.Header.SetMethod(fiber.MethodOptions) // Perform request handler(ctx) // Check result - utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + utils.AssertEqual(t, "http://localhost", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) utils.AssertEqual(t, "true", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) utils.AssertEqual(t, "3600", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) utils.AssertEqual(t, "Authentication", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) @@ -105,6 +144,57 @@ func Test_CORS_Wildcard(t *testing.T) { utils.AssertEqual(t, "X-Request-ID", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders))) } +// go test -run -v Test_CORS_Wildcard_AllowCredentials_Panic +// Test for fiber-ghsa-fmg4-x8pw-hjhg +func Test_CORS_Wildcard_AllowCredentials_Panic(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + + didPanic := false + func() { + defer func() { + if r := recover(); r != nil { + didPanic = true + } + }() + + app.Use(New(Config{ + AllowOrigins: "*", + AllowCredentials: true, + })) + }() + + if !didPanic { + t.Errorf("Expected a panic when AllowOrigins is '*' and AllowCredentials is true") + } +} + +// go test -run -v Test_CORS_Invalid_Origin_Panic +func Test_CORS_Invalid_Origin_Panic(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + + didPanic := false + func() { + defer func() { + if r := recover(); r != nil { + didPanic = true + } + }() + + app.Use(New(Config{ + AllowOrigins: "localhost", + AllowCredentials: true, + })) + }() + + if !didPanic { + t.Errorf("Expected a panic when Origin is missing scheme") + } +} + // go test -run -v Test_CORS_Subdomain func Test_CORS_Subdomain(t *testing.T) { t.Parallel() @@ -193,12 +283,9 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { shouldAllowOrigin: false, }, { - pattern: "http://*.example.com", - reqOrigin: `http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ - .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ - .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890\ - .1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com`, - shouldAllowOrigin: false, + pattern: "http://*.example.com", + reqOrigin: "http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com", + shouldAllowOrigin: true, }, { pattern: "http://example.com", @@ -471,12 +558,13 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { } // The fix for issue #2422 -func Test_CORS_AllowCredetials(t *testing.T) { +func Test_CORS_AllowCredentials(t *testing.T) { testCases := []struct { - Name string - Config Config - RequestOrigin string - ResponseOrigin string + Name string + Config Config + RequestOrigin string + ResponseOrigin string + ResponseCredentials string }{ { Name: "AllowOriginsFuncDefined", @@ -488,19 +576,35 @@ func Test_CORS_AllowCredetials(t *testing.T) { }, RequestOrigin: "http://aaa.com", // The AllowOriginsFunc config was defined, should use the real origin of the function - ResponseOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + ResponseCredentials: "true", }, { - Name: "AllowOriginsFuncNotDefined", + Name: "fiber-ghsa-fmg4-x8pw-hjhg-wildcard-credentials", Config: Config{ AllowCredentials: true, + AllowOriginsFunc: func(origin string) bool { + return true + }, + }, + RequestOrigin: "*", + ResponseOrigin: "*", + // Middleware will validate that wildcard wont set credentials to true + ResponseCredentials: "", + }, + { + Name: "AllowOriginsFuncNotDefined", + Config: Config{ + // Setting this to true will cause the middleware to panic since default AllowOrigins is "*" + AllowCredentials: false, }, RequestOrigin: "http://aaa.com", // None of the AllowOrigins or AllowOriginsFunc config was defined, should use the default origin of "*" // which will cause the CORS error in the client: // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' // when the request's credentials mode is 'include'. - ResponseOrigin: "*", + ResponseOrigin: "*", + ResponseCredentials: "", }, { Name: "AllowOriginsDefined", @@ -508,8 +612,9 @@ func Test_CORS_AllowCredetials(t *testing.T) { AllowCredentials: true, AllowOrigins: "http://aaa.com", }, - RequestOrigin: "http://aaa.com", - ResponseOrigin: "http://aaa.com", + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + ResponseCredentials: "true", }, { Name: "AllowOriginsDefined/UnallowedOrigin", @@ -517,8 +622,9 @@ func Test_CORS_AllowCredetials(t *testing.T) { AllowCredentials: true, AllowOrigins: "http://aaa.com", }, - RequestOrigin: "http://bbb.com", - ResponseOrigin: "", + RequestOrigin: "http://bbb.com", + ResponseOrigin: "", + ResponseCredentials: "", }, } @@ -536,9 +642,7 @@ func Test_CORS_AllowCredetials(t *testing.T) { handler(ctx) - if tc.Config.AllowCredentials { - utils.AssertEqual(t, "true", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) - } + utils.AssertEqual(t, tc.ResponseCredentials, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) utils.AssertEqual(t, tc.ResponseOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) }) } diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go index 8b6114bdab..313a430c77 100644 --- a/middleware/cors/utils.go +++ b/middleware/cors/utils.go @@ -1,56 +1,83 @@ package cors import ( + "net/url" "strings" ) +// matchScheme compares the scheme of the domain and pattern func matchScheme(domain, pattern string) bool { didx := strings.Index(domain, ":") pidx := strings.Index(pattern, ":") return didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx] } -// matchSubdomain compares authority with wildcard -func matchSubdomain(domain, pattern string) bool { - if !matchScheme(domain, pattern) { - return false +// validateDomain checks if the domain matches the pattern +func validateDomain(domain, pattern string) bool { + // Directly compare the domain and pattern for an exact match. + if domain == pattern { + return true } - didx := strings.Index(domain, "://") - pidx := strings.Index(pattern, "://") - if didx == -1 || pidx == -1 { - return false + + // Normalize domain and pattern to exclude schemes and ports for matching purposes + normalizedDomain := normalizeDomain(domain) + normalizedPattern := normalizeDomain(pattern) + + // Handling the case where pattern is a wildcard subdomain pattern. + if strings.HasPrefix(normalizedPattern, "*.") { + // Trim leading "*." from pattern for comparison. + trimmedPattern := normalizedPattern[2:] + + // Check if the domain ends with the trimmed pattern. + if strings.HasSuffix(normalizedDomain, trimmedPattern) { + // Ensure that the domain is not exactly the base domain. + if normalizedDomain != trimmedPattern { + // Special handling to prevent "example.com" matching "*.example.com". + if strings.TrimSuffix(normalizedDomain, trimmedPattern) != "" { + return true + } + } + } } - domAuth := domain[didx+3:] - // to avoid long loop by invalid long domain - const maxDomainLen = 253 - if len(domAuth) > maxDomainLen { - return false + + return false +} + +// normalizeDomain removes the scheme and port from the input domain +func normalizeDomain(input string) string { + // Remove scheme + input = strings.TrimPrefix(strings.TrimPrefix(input, "http://"), "https://") + + // Find and remove port, if present + if portIndex := strings.Index(input, ":"); portIndex != -1 { + input = input[:portIndex] } - patAuth := pattern[pidx+3:] - - domComp := strings.Split(domAuth, ".") - patComp := strings.Split(patAuth, ".") - const divHalf = 2 - for i := len(domComp)/divHalf - 1; i >= 0; i-- { - opp := len(domComp) - 1 - i - domComp[i], domComp[opp] = domComp[opp], domComp[i] + + return input +} + +// normalizeOrigin checks if the provided origin is in a correct format +// and normalizes it by removing any path or trailing slash. +// It returns a boolean indicating whether the origin is valid +// and the normalized origin. +func normalizeOrigin(origin string) (bool, string) { + parsedOrigin, err := url.Parse(origin) + if err != nil { + return false, "" } - for i := len(patComp)/divHalf - 1; i >= 0; i-- { - opp := len(patComp) - 1 - i - patComp[i], patComp[opp] = patComp[opp], patComp[i] + + // Validate the scheme is either http or https + if parsedOrigin.Scheme != "http" && parsedOrigin.Scheme != "https" { + return false, "" } - for i, v := range domComp { - if len(patComp) <= i { - return false - } - p := patComp[i] - if p == "*" { - return true - } - if p != v { - return false - } + // Validate there is a host present. The presence of a path, query, or fragment components + // is checked, but a trailing "/" (indicative of the root) is allowed for the path and will be normalized + if parsedOrigin.Host == "" || (parsedOrigin.Path != "" && parsedOrigin.Path != "/") || parsedOrigin.RawQuery != "" || parsedOrigin.Fragment != "" { + return false, "" } - return false + + // Normalize the origin by constructing it from the scheme and host. + // The path or trailing slash is not included in the normalized origin. + return true, strings.ToLower(parsedOrigin.Scheme) + "://" + strings.ToLower(parsedOrigin.Host) } diff --git a/middleware/cors/utils_test.go b/middleware/cors/utils_test.go new file mode 100644 index 0000000000..3acd692521 --- /dev/null +++ b/middleware/cors/utils_test.go @@ -0,0 +1,145 @@ +package cors + +import ( + "testing" +) + +// go test -run -v Test_normalizeOrigin +func Test_normalizeOrigin(t *testing.T) { + testCases := []struct { + origin string + expectedValid bool + expectedOrigin string + }{ + {"http://example.com", true, "http://example.com"}, // Simple case should work. + {"http://example.com/", true, "http://example.com"}, // Trailing slash should be removed. + {"http://example.com:3000", true, "http://example.com:3000"}, // Port should be preserved. + {"http://example.com:3000/", true, "http://example.com:3000"}, // Trailing slash should be removed. + {"http://", false, ""}, // Invalid origin should not be accepted. + {"http://example.com/path", false, ""}, // Path should not be accepted. + {"http://example.com?query=123", false, ""}, // Query should not be accepted. + {"http://example.com#fragment", false, ""}, // Fragment should not be accepted. + {"http://localhost", true, "http://localhost"}, // Localhost should be accepted. + {"http://127.0.0.1", true, "http://127.0.0.1"}, // IPv4 address should be accepted. + {"http://[::1]", true, "http://[::1]"}, // IPv6 address should be accepted. + {"http://[::1]:8080", true, "http://[::1]:8080"}, // IPv6 address with port should be accepted. + {"http://[::1]:8080/", true, "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. + {"http://[::1]:8080/path", false, ""}, // IPv6 address with port and path should not be accepted. + {"http://[::1]:8080?query=123", false, ""}, // IPv6 address with port and query should not be accepted. + {"http://[::1]:8080#fragment", false, ""}, // IPv6 address with port and fragment should not be accepted. + {"http://[::1]:8080/path?query=123#fragment", false, ""}, // IPv6 address with port, path, query, and fragment should not be accepted. + {"http://[::1]:8080/path?query=123#fragment/", false, ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. + {"http://[::1]:8080/path?query=123#fragment/invalid", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. + {"http://[::1]:8080/path?query=123#fragment/invalid/", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. + {"http://[::1]:8080/path?query=123#fragment/invalid/segment", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. + } + + for _, tc := range testCases { + valid, normalizedOrigin := normalizeOrigin(tc.origin) + + if valid != tc.expectedValid { + t.Errorf("Expected origin '%s' to be valid: %v, but got: %v", tc.origin, tc.expectedValid, valid) + } + + if normalizedOrigin != tc.expectedOrigin { + t.Errorf("Expected normalized origin '%s' for origin '%s', but got: '%s'", tc.expectedOrigin, tc.origin, normalizedOrigin) + } + } +} + +// go test -run -v Test_matchScheme +func Test_matchScheme(t *testing.T) { + testCases := []struct { + domain string + pattern string + expected bool + }{ + {"http://example.com", "http://example.com", true}, // Exact match should work. + {"https://example.com", "http://example.com", false}, // Scheme mismatch should matter. + {"http://example.com", "https://example.com", false}, // Scheme mismatch should matter. + {"http://example.com", "http://example.org", true}, // Different domains should not matter. + {"http://example.com", "http://example.com:8080", true}, // Port should not matter. + {"http://example.com:8080", "http://example.com", true}, // Port should not matter. + {"http://example.com:8080", "http://example.com:8081", true}, // Different ports should not matter. + {"http://localhost", "http://localhost", true}, // Localhost should match. + {"http://127.0.0.1", "http://127.0.0.1", true}, // IPv4 address should match. + {"http://[::1]", "http://[::1]", true}, // IPv6 address should match. + } + + for _, tc := range testCases { + result := matchScheme(tc.domain, tc.pattern) + + if result != tc.expected { + t.Errorf("Expected matchScheme('%s', '%s') to be %v, but got %v", tc.domain, tc.pattern, tc.expected, result) + } + } +} + +// go test -run -v Test_validateOrigin +func Test_validateOrigin(t *testing.T) { + testCases := []struct { + domain string + pattern string + expected bool + }{ + {"http://example.com", "http://example.com", true}, // Exact match should work. + {"https://example.com", "http://example.com", false}, // Scheme mismatch should matter in CORS context. + {"http://example.com", "https://example.com", false}, // Scheme mismatch should matter in CORS context. + {"http://example.com", "http://example.org", false}, // Different domains should not match. + {"http://example.com", "http://example.com:8080", false}, // Port mismatch should matter. + {"http://example.com:8080", "http://example.com", false}, // Port mismatch should matter. + {"http://example.com:8080", "http://example.com:8081", false}, // Different ports should not match. + {"example.com", "example.com", true}, // Simplified form, assuming scheme and port are not considered here, but in practice, they are part of the origin. + {"sub.example.com", "example.com", false}, // Subdomain should not match the base domain directly. + {"sub.example.com", "*.example.com", true}, // Correct assumption for wildcard subdomain matching. + {"example.com", "*.example.com", false}, // Base domain should not match its wildcard subdomain pattern. + {"sub.example.com", "*.com", true}, // Technically correct for pattern matching, but broad wildcard use like this is not recommended for CORS. + {"sub.sub.example.com", "*.example.com", true}, // Nested subdomain should match the wildcard pattern. + {"example.com", "*.org", false}, // Different TLDs should not match. + {"example.com", "example.org", false}, // Different domains should not match. + {"example.com:8080", "*.example.com", false}, // Different ports mean different origins. + {"example.com", "sub.example.net", false}, // Different domains should not match. + {"http://localhost", "http://localhost", true}, // Localhost should match. + {"http://127.0.0.1", "http://127.0.0.1", true}, // IPv4 address should match. + {"http://[::1]", "http://[::1]", true}, // IPv6 address should match. + } + + for _, tc := range testCases { + result := validateDomain(tc.domain, tc.pattern) + + if result != tc.expected { + t.Errorf("Expected validateOrigin('%s', '%s') to be %v, but got %v", tc.domain, tc.pattern, tc.expected, result) + } + } +} + +// go test -run -v Test_normalizeDomain +func Test_normalizeDomain(t *testing.T) { + testCases := []struct { + input string + expectedOutput string + }{ + {"http://example.com", "example.com"}, // Simple case with http scheme. + {"https://example.com", "example.com"}, // Simple case with https scheme. + {"http://example.com:3000", "example.com"}, // Case with port. + {"https://example.com:3000", "example.com"}, // Case with port and https scheme. + {"http://example.com/path", "example.com/path"}, // Case with path. + {"http://example.com?query=123", "example.com?query=123"}, // Case with query. + {"http://example.com#fragment", "example.com#fragment"}, // Case with fragment. + {"example.com", "example.com"}, // Case without scheme. + {"example.com:8080", "example.com"}, // Case without scheme but with port. + {"sub.example.com", "sub.example.com"}, // Case with subdomain. + {"sub.sub.example.com", "sub.sub.example.com"}, // Case with nested subdomain. + {"http://localhost", "localhost"}, // Case with localhost. + {"http://127.0.0.1", "127.0.0.1"}, // Case with IPv4 address. + {"http://[::1]", "[::1]"}, // Case with IPv6 address. + } + + for _, tc := range testCases { + output := normalizeDomain(tc.input) + + if output != tc.expectedOutput { + t.Errorf("Expected normalized domain '%s' for input '%s', but got: '%s'", tc.expectedOutput, tc.input, output) + } + } +} From f9fcb0297c942a84465d522a4e6487f04a372abb Mon Sep 17 00:00:00 2001 From: RW Date: Wed, 21 Feb 2024 17:03:00 +0100 Subject: [PATCH 29/46] Update app.go prepare release v2.52.1 --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 2ce88704c5..23501437b1 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.52.0" +const Version = "2.52.1" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error From 70f21d5f7eb486875d6f6f07c858c7d20e9b3f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Wed, 21 Feb 2024 21:18:56 +0100 Subject: [PATCH 30/46] fix cors domain normalize --- middleware/cors/utils.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go index 313a430c77..d1280899c9 100644 --- a/middleware/cors/utils.go +++ b/middleware/cors/utils.go @@ -49,8 +49,10 @@ func normalizeDomain(input string) string { input = strings.TrimPrefix(strings.TrimPrefix(input, "http://"), "https://") // Find and remove port, if present - if portIndex := strings.Index(input, ":"); portIndex != -1 { - input = input[:portIndex] + if len(input) > 0 && input[0] != '[' { + if portIndex := strings.Index(input, ":"); portIndex != -1 { + input = input[:portIndex] + } } return input From 0df0e0855ddaa0ab1d548d2d34b9e3ecaa1a6485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Wed, 21 Feb 2024 21:43:28 +0100 Subject: [PATCH 31/46] fix sync-docs workflow --- .github/workflows/sync-docs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index dd53569b2e..1a7076f7eb 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -1,11 +1,11 @@ name: "Sync docs" on: -# push: -# branches: -# - v2 -# paths: -# - "docs/**" + push: + branches: + - v2 + paths: + - "docs/**" release: types: [published] From ddc6b231f816f197a5f1d73825eaf0c7d55b08f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Wed, 21 Feb 2024 21:54:55 +0100 Subject: [PATCH 32/46] fix sync-docs workflow --- .github/workflows/sync-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index 1a7076f7eb..da5380b992 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.release.tag_name }} fetch-depth: 2 - name: Setup Node.js environment From d456e7d82ee087d5b2a5df5d4ab35f19c6397aae Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Fri, 1 Mar 2024 05:31:11 -0400 Subject: [PATCH 33/46] fix(middleware/cors): Validation of multiple Origins (#2883) * fix: allow origins check Refactor CORS origin validation and normalization to trim leading or trailing whitespace in the cfg.AllowOrigins string [list]. URLs with whitespace inside the URL are invalid, so the normalizeOrigin will return false because url.Parse will fail, and the middleware will panic. fixes #2882 * test: AllowOrigins with whitespace * test(middleware/cors): add benchmarks * chore: fix linter errors * test(middleware/cors): use h() instead of app.Test() * test(middleware/cors): add miltiple origins in Test_CORS_AllowOriginScheme * chore: refactor validate and normalize * test(cors/middleware): add more benchmarks --- middleware/cors/cors.go | 36 +-- middleware/cors/cors_test.go | 492 +++++++++++++++++++++++++++++++++++ 2 files changed, 513 insertions(+), 15 deletions(-) diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index 2ca3767d1f..3accc2f1dd 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -113,24 +113,32 @@ func New(config ...Config) fiber.Handler { panic("[CORS] Insecure setup, 'AllowCredentials' is set to true, and 'AllowOrigins' is set to a wildcard.") } - // Validate and normalize static AllowOrigins if not using AllowOriginsFunc - if cfg.AllowOriginsFunc == nil && cfg.AllowOrigins != "" && cfg.AllowOrigins != "*" { - validatedOrigins := []string{} - for _, origin := range strings.Split(cfg.AllowOrigins, ",") { - isValid, normalizedOrigin := normalizeOrigin(origin) + // allowOrigins is a slice of strings that contains the allowed origins + // defined in the 'AllowOrigins' configuration. + var allowOrigins []string + + // Validate and normalize static AllowOrigins + if cfg.AllowOrigins != "" && cfg.AllowOrigins != "*" { + origins := strings.Split(cfg.AllowOrigins, ",") + allowOrigins = make([]string, len(origins)) + + for i, origin := range origins { + trimmedOrigin := strings.TrimSpace(origin) + isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) + if isValid { - validatedOrigins = append(validatedOrigins, normalizedOrigin) + allowOrigins[i] = normalizedOrigin } else { - log.Warnf("[CORS] Invalid origin format in configuration: %s", origin) + log.Warnf("[CORS] Invalid origin format in configuration: %s", trimmedOrigin) panic("[CORS] Invalid origin provided in configuration") } } - cfg.AllowOrigins = strings.Join(validatedOrigins, ",") + } else { + // If AllowOrigins is set to a wildcard or not set, + // set allowOrigins to a slice with a single element + allowOrigins = []string{cfg.AllowOrigins} } - // Convert string to slice - allowOrigins := strings.Split(strings.ReplaceAll(cfg.AllowOrigins, " ", ""), ",") - // Strip white spaces allowMethods := strings.ReplaceAll(cfg.AllowMethods, " ", "") allowHeaders := strings.ReplaceAll(cfg.AllowHeaders, " ", "") @@ -165,10 +173,8 @@ func New(config ...Config) fiber.Handler { // Run AllowOriginsFunc if the logic for // handling the value in 'AllowOrigins' does // not result in allowOrigin being set. - if allowOrigin == "" && cfg.AllowOriginsFunc != nil { - if cfg.AllowOriginsFunc(originHeader) { - allowOrigin = originHeader - } + if allowOrigin == "" && cfg.AllowOriginsFunc != nil && cfg.AllowOriginsFunc(originHeader) { + allowOrigin = originHeader } // Simple request diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index 9fc2852556..23692c3f84 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -307,6 +307,21 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { reqOrigin: "http://ccc.bbb.example.com", shouldAllowOrigin: false, }, + { + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://domain-2.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://domain-1.com,http://example.com", + reqOrigin: "http://domain-1.com", + shouldAllowOrigin: true, + }, } for _, tt := range tests { @@ -452,6 +467,33 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { RequestOrigin: "http://aaa.com", ResponseOrigin: "http://aaa.com", }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/NoWhitespace/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com,http://bbb.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "http://bbb.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/NoWhitespace/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com,http://bbb.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://ccc.com", + ResponseOrigin: "", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/Whitespace/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com, http://bbb.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, { Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/OriginNotAllowed", Config: Config{ @@ -647,3 +689,453 @@ func Test_CORS_AllowCredentials(t *testing.T) { }) } } + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandler -benchmem -count=4 +func Benchmark_CORS_NewHandler(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://localhost") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://localhost") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerSingleOrigin -benchmem -count=4 +func Benchmark_CORS_NewHandlerSingleOrigin(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerSingleOriginParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerSingleOriginParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerWildcard -benchmem -count=4 +func Benchmark_CORS_NewHandlerWildcard(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerWildcardParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerWildcardParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflight -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflight(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightSingleOrigin -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightSingleOrigin(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightSingleOriginParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightSingleOriginParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightWildcard -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightWildcard(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightWildcardParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightWildcardParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} From 109e91a630679dc7739e80ebdf8355ace8aafcd4 Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 2 Mar 2024 18:56:50 +0100 Subject: [PATCH 34/46] prepare release v2.52.2 --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 23501437b1..178125b872 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.52.1" +const Version = "2.52.2" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error From 68d90cd6b24308ebe3cf77a7ecb11c9683cbba8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Sat, 2 Mar 2024 19:13:07 +0100 Subject: [PATCH 35/46] refactor(docs): deactivate docs sync for v2 --- .github/workflows/sync-docs.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index da5380b992..2d61880307 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -1,13 +1,13 @@ name: "Sync docs" on: - push: - branches: - - v2 - paths: - - "docs/**" - release: - types: [published] +# push: +# branches: +# - v2 +# paths: +# - "docs/**" +# release: +# types: [published] jobs: sync-docs: From d2b19e290d563ed80b188d875c0121059b449fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Sat, 2 Mar 2024 19:14:29 +0100 Subject: [PATCH 36/46] refactor(docs): deactivate docs sync for v2 --- .github/workflows/sync-docs.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index 2d61880307..4016883fda 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -1,16 +1,17 @@ name: "Sync docs" on: -# push: -# branches: -# - v2 -# paths: -# - "docs/**" -# release: -# types: [published] + push: + branches: + - v2 + paths: + - "docs/**" + release: + types: [published] jobs: sync-docs: + if: false runs-on: ubuntu-latest steps: - name: Checkout From 1aac6f618b6bd3e159d2449acb4232a2170b976b Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Sun, 17 Mar 2024 09:43:16 -0300 Subject: [PATCH 37/46] fix(middleware/cors): Handling and wildcard subdomain matching (#2915) * fix: allow origins check Refactor CORS origin validation and normalization to trim leading or trailing whitespace in the cfg.AllowOrigins string [list]. URLs with whitespace inside the URL are invalid, so the normalizeOrigin will return false because url.Parse will fail, and the middleware will panic. fixes #2882 * test: AllowOrigins with whitespace * test(middleware/cors): add benchmarks * chore: fix linter errors * test(middleware/cors): use h() instead of app.Test() * test(middleware/cors): add miltiple origins in Test_CORS_AllowOriginScheme * chore: refactor validate and normalize * test(cors/middleware): add more benchmarks * fix(middleware/cors): handling and wildcard subdomain matching docs(middleware/cors): add How it works and Security Considerations * chore: grammar * Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * chore: fix misspelling * test(middleware/cors): combine Invalid_Origins tests * refactor(middleware/cors): headers handling * docs(middleware/cors): Update AllowOrigins description * chore: merge * perf(middleware/cors): optimize handler * perf(middleware/cors): optimize handler * chore(middleware/cors): ipdate origin handling logic * chore(middleware/cors): fix header capitalization * docs(middleware/cors): improve sercuity notes * docs(middleware/cors): Improve security notes * docs(middleware/cors): improve CORS overview * docs(middleware/cors): fix ordering of how it works * docs(middleware/cors): add additional info to How to works * docs(middleware/cors): rm space * docs(middleware/cors): add validation for AllowOrigins origins to overview * docs(middleware/cors): update ExposeHeaders and MaxAge descriptions * docs(middleware/cors): Add dynamic origin validation example * docs(middleware/cors): Improve security notes and fix header capitalization * docs(middleware/cors): configuration examples * docs(middleware/cors): `"*"` --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/api/middleware/cors.md | 147 ++++++++++++++++++++++++++---- middleware/cors/cors.go | 166 +++++++++++++++++++++------------- middleware/cors/cors_test.go | 102 +++++++++++++++++---- middleware/cors/utils.go | 48 ++++------ middleware/cors/utils_test.go | 38 -------- 5 files changed, 331 insertions(+), 170 deletions(-) diff --git a/docs/api/middleware/cors.md b/docs/api/middleware/cors.md index ca250833d6..882a74808b 100644 --- a/docs/api/middleware/cors.md +++ b/docs/api/middleware/cors.md @@ -4,13 +4,15 @@ id: cors # CORS -CORS middleware for [Fiber](https://github.com/gofiber/fiber) that can be used to enable [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options. +CORS (Cross-Origin Resource Sharing) is a middleware for [Fiber](https://github.com/gofiber/fiber) that allows servers to specify who can access its resources and how. It's not a security feature, but a way to relax the security model of web browsers for cross-origin requests. You can learn more about CORS on [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). -The middleware conforms to the `access-control-allow-origin` specification by parsing `AllowOrigins`. First, the middleware checks if there is a matching allowed origin for the requesting 'origin' header. If there is a match, it returns exactly one matching domain from the list of allowed origins. +This middleware works by adding CORS headers to responses from your Fiber application. These headers specify which origins, methods, and headers are allowed for cross-origin requests. It also handles preflight requests, which are a CORS mechanism to check if the actual request is safe to send. -For more control, `AllowOriginsFunc` can be used to programatically determine if an origin is allowed. If no match was found in `AllowOrigins` and if `AllowOriginsFunc` returns true then the 'access-control-allow-origin' response header is set to the 'origin' request header. +The middleware uses the `AllowOrigins` option to control which origins can make cross-origin requests. It supports single origin, multiple origins, subdomain matching, and wildcard origin. It also allows programmatic origin validation with the `AllowOriginsFunc` option. -When defining your Origins make sure they are properly formatted. The middleware validates and normalizes the provided origins, ensuring they're in the correct format by checking for valid schemes (http or https), and removing any trailing slashes. +To ensure that the provided `AllowOrigins` origins are correctly formatted, this middleware validates and normalizes them. It checks for valid schemes, i.e., HTTP or HTTPS, and it will automatically remove trailing slashes. If the provided origin is invalid, the middleware will panic. + +When configuring CORS, it's important to avoid [common pitfalls](#common-pitfalls) like using a wildcard origin with credentials, being overly permissive with origins, and inadequate validation with `AllowOriginsFunc`. Misconfiguration can expose your application to various security risks. ## Signatures @@ -31,6 +33,16 @@ import ( After you initiate your Fiber app, you can use the following possibilities: +### Basic usage + +To use the default configuration, simply use `cors.New()`. This will allow wildcard origins '*', all methods, no credentials, and no headers or exposed headers. + +```go +app.Use(cors.New()) +``` + +### Custom configuration (specific origins, headers, etc.) + ```go // Initialize default config app.Use(cors.New()) @@ -38,27 +50,50 @@ app.Use(cors.New()) // Or extend your config for customization app.Use(cors.New(cors.Config{ AllowOrigins: "https://gofiber.io, https://gofiber.net", - AllowHeaders: "Origin, Content-Type, Accept", + AllowHeaders: "Origin, Content-Type, Accept", })) ``` -Using the `AllowOriginsFunc` function. In this example any origin will be allowed via CORS. +### Dynamic origin validation + +You can use `AllowOriginsFunc` to programmatically determine whether to allow a request based on its origin. This is useful when you need to validate origins against a database or other dynamic sources. The function should return `true` if the origin is allowed, and `false` otherwise. -For example, if a browser running on `http://localhost:3000` sends a request, this will be accepted and the `access-control-allow-origin` response header will be set to `http://localhost:3000`. +Be sure to review the [security considerations](#security-considerations) when using `AllowOriginsFunc`. -**Note: Using this feature is discouraged in production and it's best practice to explicitly set CORS origins via `AllowOrigins`.** +:::caution +Never allow `AllowOriginsFunc` to return `true` for all origins. This is particularly crucial when `AllowCredentials` is set to `true`. Doing so can bypass the restriction of using a wildcard origin with credentials, exposing your application to serious security threats. + +If you need to allow wildcard origins, use `AllowOrigins` with a wildcard `"*"` instead of `AllowOriginsFunc`. +::: ```go -app.Use(cors.New()) +// dbCheckOrigin checks if the origin is in the list of allowed origins in the database. +func dbCheckOrigin(db *sql.DB, origin string) bool { + // Placeholder query - adjust according to your database schema and query needs + query := "SELECT COUNT(*) FROM allowed_origins WHERE origin = $1" + + var count int + err := db.QueryRow(query, origin).Scan(&count) + if err != nil { + // Handle error (e.g., log it); for simplicity, we return false here + return false + } + + return count > 0 +} + +// ... app.Use(cors.New(cors.Config{ - AllowOriginsFunc: func(origin string) bool { - return os.Getenv("ENVIRONMENT") == "development" - }, + AllowOriginsFunc: func(origin string) bool { + return dbCheckOrigin(db, origin) + }, })) ``` -**Note: The following configuration is considered insecure and will result in a panic.** +### Prohibited usage + +The following example is prohibited because it can expose your application to security risks. It sets `AllowOrigins` to `"*"` (a wildcard) and `AllowCredentials` to `true`. ```go app.Use(cors.New(cors.Config{ @@ -67,18 +102,24 @@ app.Use(cors.New(cors.Config{ })) ``` +This will result in the following panic: + +``` +panic: [CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to `"*"`. +``` + ## Config | Property | Type | Description | Default | |:-----------------|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------| | Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| AllowOriginsFunc | `func(origin string) bool` | AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' response header to the 'origin' request header when returned true. This allows for dynamic evaluation of allowed origins. Note if AllowCredentials is true, wildcard origins will be not have the 'access-control-allow-credentials' header set to 'true'. | `nil` | -| AllowOrigins | `string` | AllowOrigin defines a comma separated list of origins that may access the resource. | `"*"` | +| AllowOriginsFunc | `func(origin string) bool` | `AllowOriginsFunc` is a function that dynamically determines whether to allow a request based on its origin. If this function returns `true`, the 'Access-Control-Allow-Origin' response header will be set to the request's 'origin' header. This function is only used if the request's origin doesn't match any origin in `AllowOrigins`. | `nil` | +| AllowOrigins | `string` | AllowOrigins defines a comma separated list of origins that may access the resource. This supports subdomain matching, so you can use a value like "https://*.example.com" to allow any subdomain of example.com to submit requests. | `"*"` | | AllowMethods | `string` | AllowMethods defines a list of methods allowed when accessing the resource. This is used in response to a preflight request. | `"GET,POST,HEAD,PUT,DELETE,PATCH"` | | AllowHeaders | `string` | AllowHeaders defines a list of request headers that can be used when making the actual request. This is in response to a preflight request. | `""` | -| AllowCredentials | `bool` | AllowCredentials indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials. Note: If true, AllowOrigins cannot be set to a wildcard ("*") to prevent security vulnerabilities. | `false` | -| ExposeHeaders | `string` | ExposeHeaders defines a whitelist headers that clients are allowed to access. | `""` | -| MaxAge | `int` | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. If you pass MaxAge 0, Access-Control-Max-Age header will not be added and browser will use 5 seconds by default. To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header 0. | `0` | +| AllowCredentials | `bool` | AllowCredentials indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials. Note: If true, AllowOrigins cannot be set to a wildcard (`"*"`) to prevent security vulnerabilities. | `false` | +| ExposeHeaders | `string` | ExposeHeaders defines whitelist headers that clients are allowed to access. | `""` | +| MaxAge | `int` | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. If you pass MaxAge 0, the Access-Control-Max-Age header will not be added and the browser will use 5 seconds by default. To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header to 0. | `0` | ## Default Config @@ -101,3 +142,73 @@ var ConfigDefault = Config{ MaxAge: 0, } ``` + +## Subdomain Matching + +The `AllowOrigins` configuration supports matching subdomains at any level. This means you can use a value like `"https://*.example.com"` to allow any subdomain of `example.com` to submit requests, including multiple subdomain levels such as `"https://sub.sub.example.com"`. + +### Example + +If you want to allow CORS requests from any subdomain of `example.com`, including nested subdomains, you can configure the `AllowOrigins` like so: + +```go +app.Use(cors.New(cors.Config{ + AllowOrigins: "https://*.example.com", +})) +``` + +# How It Works + +The CORS middleware works by adding the necessary CORS headers to responses from your Fiber application. These headers tell browsers what origins, methods, and headers are allowed for cross-origin requests. + +When a request comes in, the middleware first checks if it's a preflight request, which is a CORS mechanism to determine whether the actual request is safe to send. Preflight requests are HTTP OPTIONS requests with specific CORS headers. If it's a preflight request, the middleware responds with the appropriate CORS headers and ends the request. + +If it's not a preflight request, the middleware adds the CORS headers to the response and passes the request to the next handler. The actual CORS headers added depend on the configuration of the middleware. + +The `AllowOrigins` option controls which origins can make cross-origin requests. The middleware handles different `AllowOrigins` configurations as follows: + +- **Single origin:** If `AllowOrigins` is set to a single origin like `"http://www.example.com"`, and that origin matches the origin of the incoming request, the middleware adds the header `Access-Control-Allow-Origin: http://www.example.com` to the response. + +- **Multiple origins:** If `AllowOrigins` is set to multiple origins like `"https://example.com, https://www.example.com"`, the middleware picks the origin that matches the origin of the incoming request. + +- **Subdomain matching:** If `AllowOrigins` includes `"https://*.example.com"`, a subdomain like `https://sub.example.com` will be matched and `"https://sub.example.com"` will be the header. This will also match `https://sub.sub.example.com` and so on, but not `https://example.com`. + +- **Wildcard origin:** If `AllowOrigins` is set to `"*"`, the middleware uses that and adds the header `Access-Control-Allow-Origin: *` to the response. + +In all cases above, except the **Wildcard origin**, the middleware will either add the `Access-Control-Allow-Origin` header to the response matching the origin of the incoming request, or it will not add the header at all if the origin is not allowed. + +- **Programmatic origin validation:**: The middleware also handles the `AllowOriginsFunc` option, which allows you to programmatically determine if an origin is allowed. If `AllowOriginsFunc` returns `true` for an origin, the middleware sets the `Access-Control-Allow-Origin` header to that origin. + +The `AllowMethods` option controls which HTTP methods are allowed. For example, if `AllowMethods` is set to `"GET, POST"`, the middleware adds the header `Access-Control-Allow-Methods: GET, POST` to the response. + +The `AllowHeaders` option specifies which headers are allowed in the actual request. The middleware sets the Access-Control-Allow-Headers response header to the value of `AllowHeaders`. This informs the client which headers it can use in the actual request. + +The `AllowCredentials` option indicates whether the response to the request can be exposed when the credentials flag is true. If `AllowCredentials` is set to `true`, the middleware adds the header `Access-Control-Allow-Credentials: true` to the response. To prevent security vulnerabilities, `AllowCredentials` cannot be set to `true` if `AllowOrigins` is set to a wildcard (`*`). + +The `ExposeHeaders` option defines a whitelist of headers that clients are allowed to access. If `ExposeHeaders` is set to `"X-Custom-Header"`, the middleware adds the header `Access-Control-Expose-Headers: X-Custom-Header` to the response. + +The `MaxAge` option indicates how long the results of a preflight request can be cached. If `MaxAge` is set to `3600`, the middleware adds the header `Access-Control-Max-Age: 3600` to the response. + +The `Vary` header is used in this middleware to inform the client that the server's response to a request. For or both preflight and actual requests, the Vary header is set to `Access-Control-Request-Method` and `Access-Control-Request-Headers`. For preflight requests, the Vary header is also set to `Origin`. The `Vary` header is important for caching. It helps caches (like a web browser's cache or a CDN) determine when a cached response can be used in response to a future request, and when the server needs to be queried for a new response. + +## Security Considerations + +When configuring CORS, misconfiguration can potentially expose your application to various security risks. Here are some secure configurations and common pitfalls to avoid: + +### Secure Configurations + +- **Specify Allowed Origins**: Instead of using a wildcard (`"*"`), specify the exact domains allowed to make requests. For example, `AllowOrigins: "https://www.example.com, https://api.example.com"` ensures only these domains can make cross-origin requests to your application. + +- **Use Credentials Carefully**: If your application needs to support credentials in cross-origin requests, ensure `AllowCredentials` is set to `true` and specify exact origins in `AllowOrigins`. Do not use a wildcard origin in this case. + +- **Limit Exposed Headers**: Only whitelist headers that are necessary for the client-side application by setting `ExposeHeaders` appropriately. This minimizes the risk of exposing sensitive information. + +### Common Pitfalls + +- **Wildcard Origin with Credentials**: Setting `AllowOrigins` to `"*"` (a wildcard) and `AllowCredentials` to `true` is a common misconfiguration. This combination is prohibited because it can expose your application to security risks. + +- **Overly Permissive Origins**: Specifying too many origins or using overly broad patterns (e.g., `https://*.example.com`) can inadvertently allow malicious sites to interact with your application. Be as specific as possible with allowed origins. + +- **Inadequate `AllowOriginsFunc` Validation**: When using `AllowOriginsFunc` for dynamic origin validation, ensure the function includes robust checks to prevent unauthorized origins from being accepted. Overly permissive validation can lead to security vulnerabilities. Never allow `AllowOriginsFunc` to return `true` for all origins. This is particularly crucial when `AllowCredentials` is set to `true`. Doing so can bypass the restriction of using a wildcard origin with credentials, exposing your application to serious security threats. If you need to allow wildcard origins, use `AllowOrigins` with a wildcard `"*"` instead of `AllowOriginsFunc`. + +Remember, the key to secure CORS configuration is specificity and caution. By carefully selecting which origins, methods, and headers are allowed, you can help protect your application from cross-origin attacks. \ No newline at end of file diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index 3accc2f1dd..e27e74cba8 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -15,10 +15,10 @@ type Config struct { // Optional. Default: nil Next func(c *fiber.Ctx) bool - // AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' + // AllowOriginsFunc defines a function that will set the 'Access-Control-Allow-Origin' // response header to the 'origin' request header when returned true. This allows for // dynamic evaluation of allowed origins. Note if AllowCredentials is true, wildcard origins - // will be not have the 'access-control-allow-credentials' header set to 'true'. + // will be not have the 'Access-Control-Allow-Credentials' header set to 'true'. // // Optional. Default: nil AllowOriginsFunc func(origin string) bool @@ -115,28 +115,43 @@ func New(config ...Config) fiber.Handler { // allowOrigins is a slice of strings that contains the allowed origins // defined in the 'AllowOrigins' configuration. - var allowOrigins []string + allowOrigins := []string{} + allowSOrigins := []subdomain{} + allowAllOrigins := false + + // processOrigin processes an origin string, normalizes it and checks its validity + // it will panic if the origin is invalid + processOrigin := func(origin string) (string, bool) { + trimmedOrigin := strings.TrimSpace(origin) + isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) + if !isValid { + log.Warnf("[CORS] Invalid origin format in configuration: %s", trimmedOrigin) + panic("[CORS] Invalid origin provided in configuration") + } + return normalizedOrigin, true + } // Validate and normalize static AllowOrigins if cfg.AllowOrigins != "" && cfg.AllowOrigins != "*" { origins := strings.Split(cfg.AllowOrigins, ",") - allowOrigins = make([]string, len(origins)) - - for i, origin := range origins { - trimmedOrigin := strings.TrimSpace(origin) - isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) - - if isValid { - allowOrigins[i] = normalizedOrigin + for _, origin := range origins { + if i := strings.Index(origin, "://*."); i != -1 { + normalizedOrigin, isValid := processOrigin(origin[:i+3] + origin[i+4:]) + if !isValid { + continue + } + sd := subdomain{prefix: normalizedOrigin[:i+3], suffix: normalizedOrigin[i+3:]} + allowSOrigins = append(allowSOrigins, sd) } else { - log.Warnf("[CORS] Invalid origin format in configuration: %s", trimmedOrigin) - panic("[CORS] Invalid origin provided in configuration") + normalizedOrigin, isValid := processOrigin(origin) + if !isValid { + continue + } + allowOrigins = append(allowOrigins, normalizedOrigin) } } - } else { - // If AllowOrigins is set to a wildcard or not set, - // set allowOrigins to a slice with a single element - allowOrigins = []string{cfg.AllowOrigins} + } else if cfg.AllowOrigins == "*" { + allowAllOrigins = true } // Strip white spaces @@ -155,18 +170,36 @@ func New(config ...Config) fiber.Handler { } // Get originHeader header - originHeader := c.Get(fiber.HeaderOrigin) + originHeader := strings.ToLower(c.Get(fiber.HeaderOrigin)) + + // If the request does not have an Origin header, the request is outside the scope of CORS + if originHeader == "" { + return c.Next() + } + + // Set default allowOrigin to empty string allowOrigin := "" // Check allowed origins - for _, origin := range allowOrigins { - if origin == "*" { - allowOrigin = "*" - break + if allowAllOrigins { + allowOrigin = "*" + } else { + // Check if the origin is in the list of allowed origins + for _, origin := range allowOrigins { + if origin == originHeader { + allowOrigin = originHeader + break + } } - if validateDomain(originHeader, origin) { - allowOrigin = originHeader - break + + // Check if the origin is in the list of allowed subdomains + if allowOrigin == "" { + for _, sOrigin := range allowSOrigins { + if sOrigin.match(originHeader) { + allowOrigin = originHeader + break + } + } } } @@ -179,56 +212,63 @@ func New(config ...Config) fiber.Handler { // Simple request if c.Method() != fiber.MethodOptions { - c.Vary(fiber.HeaderOrigin) - c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) - - if cfg.AllowCredentials { - c.Set(fiber.HeaderAccessControlAllowCredentials, "true") - } - if exposeHeaders != "" { - c.Set(fiber.HeaderAccessControlExposeHeaders, exposeHeaders) - } + setCORSHeaders(c, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge, cfg) return c.Next() } // Preflight request - c.Vary(fiber.HeaderOrigin) c.Vary(fiber.HeaderAccessControlRequestMethod) c.Vary(fiber.HeaderAccessControlRequestHeaders) - c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) - c.Set(fiber.HeaderAccessControlAllowMethods, allowMethods) - if cfg.AllowCredentials { - // When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*' - if allowOrigin != "*" && allowOrigin != "" { - c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) - c.Set(fiber.HeaderAccessControlAllowCredentials, "true") - } else if allowOrigin == "*" { - log.Warn("[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.") - } - } else { - // For non-credential requests, it's safe to set to '*' or specific origins + setCORSHeaders(c, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge, cfg) + + // Send 204 No Content + return c.SendStatus(fiber.StatusNoContent) + } +} + +// Function to set CORS headers +func setCORSHeaders(c *fiber.Ctx, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge string, cfg Config) { + c.Vary(fiber.HeaderOrigin) + + if cfg.AllowCredentials { + // When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*' + if allowOrigin != "*" && allowOrigin != "" { + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + c.Set(fiber.HeaderAccessControlAllowCredentials, "true") + } else if allowOrigin == "*" { c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + log.Warn("[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.") } + } else if len(allowOrigin) > 0 { + // For non-credential requests, it's safe to set to '*' or specific origins + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + } - // Set Allow-Headers if not empty - if allowHeaders != "" { - c.Set(fiber.HeaderAccessControlAllowHeaders, allowHeaders) - } else { - h := c.Get(fiber.HeaderAccessControlRequestHeaders) - if h != "" { - c.Set(fiber.HeaderAccessControlAllowHeaders, h) - } - } + // Set Allow-Methods if not empty + if allowMethods != "" { + c.Set(fiber.HeaderAccessControlAllowMethods, allowMethods) + } - // Set MaxAge is set - if cfg.MaxAge > 0 { - c.Set(fiber.HeaderAccessControlMaxAge, maxAge) - } else if cfg.MaxAge < 0 { - c.Set(fiber.HeaderAccessControlMaxAge, "0") + // Set Allow-Headers if not empty + if allowHeaders != "" { + c.Set(fiber.HeaderAccessControlAllowHeaders, allowHeaders) + } else { + h := c.Get(fiber.HeaderAccessControlRequestHeaders) + if h != "" { + c.Set(fiber.HeaderAccessControlAllowHeaders, h) } + } - // Send 204 No Content - return c.SendStatus(fiber.StatusNoContent) + // Set MaxAge if set + if cfg.MaxAge > 0 { + c.Set(fiber.HeaderAccessControlMaxAge, maxAge) + } else if cfg.MaxAge < 0 { + c.Set(fiber.HeaderAccessControlMaxAge, "0") + } + + // Set Expose-Headers if not empty + if exposeHeaders != "" { + c.Set(fiber.HeaderAccessControlExposeHeaders, exposeHeaders) } } diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index 23692c3f84..ff5cdd7c25 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -49,6 +49,7 @@ func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { // Test default GET response headers ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") h(ctx) utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) @@ -58,6 +59,7 @@ func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { // Test default OPTIONS (preflight) response headers ctx = &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") h(ctx) utils.AssertEqual(t, "GET,POST,HEAD,PUT,DELETE,PATCH", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods))) @@ -98,6 +100,7 @@ func Test_CORS_Wildcard(t *testing.T) { // Test non OPTIONS (preflight) response headers ctx = &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") handler(ctx) utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) @@ -137,6 +140,7 @@ func Test_CORS_Origin_AllowCredentials(t *testing.T) { // Test non OPTIONS (preflight) response headers ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") ctx.Request.Header.SetMethod(fiber.MethodGet) handler(ctx) @@ -171,27 +175,39 @@ func Test_CORS_Wildcard_AllowCredentials_Panic(t *testing.T) { } // go test -run -v Test_CORS_Invalid_Origin_Panic -func Test_CORS_Invalid_Origin_Panic(t *testing.T) { +func Test_CORS_Invalid_Origins_Panic(t *testing.T) { t.Parallel() - // New fiber instance - app := fiber.New() - didPanic := false - func() { - defer func() { - if r := recover(); r != nil { - didPanic = true - } - }() + invalidOrigins := []string{ + "localhost", + "http://foo.[a-z]*.example.com", + "http://*", + "https://*", + "invalid url", + // add more invalid origins as needed + } - app.Use(New(Config{ - AllowOrigins: "localhost", - AllowCredentials: true, - })) - }() + for _, origin := range invalidOrigins { + // New fiber instance + app := fiber.New() - if !didPanic { - t.Errorf("Expected a panic when Origin is missing scheme") + didPanic := false + func() { + defer func() { + if r := recover(); r != nil { + didPanic = true + } + }() + + app.Use(New(Config{ + AllowOrigins: origin, + AllowCredentials: true, + })) + }() + + if !didPanic { + t.Errorf("Expected a panic for invalid origin: %s", origin) + } } } @@ -221,6 +237,18 @@ func Test_CORS_Subdomain(t *testing.T) { ctx.Request.Reset() ctx.Response.Reset() + // Make request with domain only (disallowed) + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") + + handler(ctx) + + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + // Make request with allowed origin ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) @@ -293,7 +321,7 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { shouldAllowOrigin: false, }, { - pattern: "https://*--aaa.bbb.com", + pattern: "https://--aaa.bbb.com", reqOrigin: "https://prod-preview--aaa.bbb.com", shouldAllowOrigin: false, }, @@ -303,8 +331,13 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { shouldAllowOrigin: true, }, { - pattern: "http://foo.[a-z]*.example.com", - reqOrigin: "http://ccc.bbb.example.com", + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://domain-2.com", shouldAllowOrigin: false, }, { @@ -345,6 +378,35 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { } } +func Test_CORS_AllowOriginHeader_NoMatch(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + app.Use("/", New(Config{ + AllowOrigins: "http://example-1.com, https://example-1.com", + })) + + // Get handler pointer + handler := app.Handler() + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + var headerExists bool + ctx.Response.Header.VisitAll(func(key, _ []byte) { + if string(key) == fiber.HeaderAccessControlAllowOrigin { + headerExists = true + } + }) + utils.AssertEqual(t, false, headerExists, "Access-Control-Allow-Origin header should not be set") +} + // go test -run Test_CORS_Next func Test_CORS_Next(t *testing.T) { t.Parallel() diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go index d1280899c9..443e648903 100644 --- a/middleware/cors/utils.go +++ b/middleware/cors/utils.go @@ -12,37 +12,6 @@ func matchScheme(domain, pattern string) bool { return didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx] } -// validateDomain checks if the domain matches the pattern -func validateDomain(domain, pattern string) bool { - // Directly compare the domain and pattern for an exact match. - if domain == pattern { - return true - } - - // Normalize domain and pattern to exclude schemes and ports for matching purposes - normalizedDomain := normalizeDomain(domain) - normalizedPattern := normalizeDomain(pattern) - - // Handling the case where pattern is a wildcard subdomain pattern. - if strings.HasPrefix(normalizedPattern, "*.") { - // Trim leading "*." from pattern for comparison. - trimmedPattern := normalizedPattern[2:] - - // Check if the domain ends with the trimmed pattern. - if strings.HasSuffix(normalizedDomain, trimmedPattern) { - // Ensure that the domain is not exactly the base domain. - if normalizedDomain != trimmedPattern { - // Special handling to prevent "example.com" matching "*.example.com". - if strings.TrimSuffix(normalizedDomain, trimmedPattern) != "" { - return true - } - } - } - } - - return false -} - // normalizeDomain removes the scheme and port from the input domain func normalizeDomain(input string) string { // Remove scheme @@ -73,6 +42,13 @@ func normalizeOrigin(origin string) (bool, string) { return false, "" } + // Don't allow a wildcard with a protocol + // wildcards cannot be used within any other value. For example, the following header is not valid: + // Access-Control-Allow-Origin: https://* + if strings.Contains(parsedOrigin.Host, "*") { + return false, "" + } + // Validate there is a host present. The presence of a path, query, or fragment components // is checked, but a trailing "/" (indicative of the root) is allowed for the path and will be normalized if parsedOrigin.Host == "" || (parsedOrigin.Path != "" && parsedOrigin.Path != "/") || parsedOrigin.RawQuery != "" || parsedOrigin.Fragment != "" { @@ -83,3 +59,13 @@ func normalizeOrigin(origin string) (bool, string) { // The path or trailing slash is not included in the normalized origin. return true, strings.ToLower(parsedOrigin.Scheme) + "://" + strings.ToLower(parsedOrigin.Host) } + +type subdomain struct { + // The wildcard pattern + prefix string + suffix string +} + +func (s subdomain) match(o string) bool { + return len(o) >= len(s.prefix)+len(s.suffix) && strings.HasPrefix(o, s.prefix) && strings.HasSuffix(o, s.suffix) +} diff --git a/middleware/cors/utils_test.go b/middleware/cors/utils_test.go index 3acd692521..adc729d05f 100644 --- a/middleware/cors/utils_test.go +++ b/middleware/cors/utils_test.go @@ -75,44 +75,6 @@ func Test_matchScheme(t *testing.T) { } } -// go test -run -v Test_validateOrigin -func Test_validateOrigin(t *testing.T) { - testCases := []struct { - domain string - pattern string - expected bool - }{ - {"http://example.com", "http://example.com", true}, // Exact match should work. - {"https://example.com", "http://example.com", false}, // Scheme mismatch should matter in CORS context. - {"http://example.com", "https://example.com", false}, // Scheme mismatch should matter in CORS context. - {"http://example.com", "http://example.org", false}, // Different domains should not match. - {"http://example.com", "http://example.com:8080", false}, // Port mismatch should matter. - {"http://example.com:8080", "http://example.com", false}, // Port mismatch should matter. - {"http://example.com:8080", "http://example.com:8081", false}, // Different ports should not match. - {"example.com", "example.com", true}, // Simplified form, assuming scheme and port are not considered here, but in practice, they are part of the origin. - {"sub.example.com", "example.com", false}, // Subdomain should not match the base domain directly. - {"sub.example.com", "*.example.com", true}, // Correct assumption for wildcard subdomain matching. - {"example.com", "*.example.com", false}, // Base domain should not match its wildcard subdomain pattern. - {"sub.example.com", "*.com", true}, // Technically correct for pattern matching, but broad wildcard use like this is not recommended for CORS. - {"sub.sub.example.com", "*.example.com", true}, // Nested subdomain should match the wildcard pattern. - {"example.com", "*.org", false}, // Different TLDs should not match. - {"example.com", "example.org", false}, // Different domains should not match. - {"example.com:8080", "*.example.com", false}, // Different ports mean different origins. - {"example.com", "sub.example.net", false}, // Different domains should not match. - {"http://localhost", "http://localhost", true}, // Localhost should match. - {"http://127.0.0.1", "http://127.0.0.1", true}, // IPv4 address should match. - {"http://[::1]", "http://[::1]", true}, // IPv6 address should match. - } - - for _, tc := range testCases { - result := validateDomain(tc.domain, tc.pattern) - - if result != tc.expected { - t.Errorf("Expected validateOrigin('%s', '%s') to be %v, but got %v", tc.domain, tc.pattern, tc.expected, result) - } - } -} - // go test -run -v Test_normalizeDomain func Test_normalizeDomain(t *testing.T) { testCases := []struct { From 1607d872d9a825c1d99cf4894c429c2765cb01b6 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Wed, 20 Mar 2024 10:57:29 -0300 Subject: [PATCH 38/46] fix(middleware/cors): Categorize requests correctly (#2921) * fix(middleware/cors): categorise requests correctly * test(middleware/cors): improve test coverage for request types * test(middleware/cors): Add subdomain matching tests * test(middleware/cors): parallel tests for CORS headers based on request type * test(middleware/cors): Add benchmark for CORS subdomain matching * test(middleware/cors): cover additiona test cases * refactor(middleware/cors): origin validation and normalization --- middleware/cors/cors.go | 40 ++++------ middleware/cors/cors_test.go | 142 ++++++++++++++++++++++++++++++++-- middleware/cors/utils_test.go | 88 +++++++++++++++++++++ 3 files changed, 238 insertions(+), 32 deletions(-) diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index e27e74cba8..7debfdfaa0 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -119,33 +119,23 @@ func New(config ...Config) fiber.Handler { allowSOrigins := []subdomain{} allowAllOrigins := false - // processOrigin processes an origin string, normalizes it and checks its validity - // it will panic if the origin is invalid - processOrigin := func(origin string) (string, bool) { - trimmedOrigin := strings.TrimSpace(origin) - isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) - if !isValid { - log.Warnf("[CORS] Invalid origin format in configuration: %s", trimmedOrigin) - panic("[CORS] Invalid origin provided in configuration") - } - return normalizedOrigin, true - } - // Validate and normalize static AllowOrigins if cfg.AllowOrigins != "" && cfg.AllowOrigins != "*" { origins := strings.Split(cfg.AllowOrigins, ",") for _, origin := range origins { if i := strings.Index(origin, "://*."); i != -1 { - normalizedOrigin, isValid := processOrigin(origin[:i+3] + origin[i+4:]) + trimmedOrigin := strings.TrimSpace(origin[:i+3] + origin[i+4:]) + isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) if !isValid { - continue + panic("[CORS] Invalid origin format in configuration: " + trimmedOrigin) } sd := subdomain{prefix: normalizedOrigin[:i+3], suffix: normalizedOrigin[i+3:]} allowSOrigins = append(allowSOrigins, sd) } else { - normalizedOrigin, isValid := processOrigin(origin) + trimmedOrigin := strings.TrimSpace(origin) + isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) if !isValid { - continue + panic("[CORS] Invalid origin format in configuration: " + trimmedOrigin) } allowOrigins = append(allowOrigins, normalizedOrigin) } @@ -172,8 +162,9 @@ func New(config ...Config) fiber.Handler { // Get originHeader header originHeader := strings.ToLower(c.Get(fiber.HeaderOrigin)) - // If the request does not have an Origin header, the request is outside the scope of CORS - if originHeader == "" { + // If the request does not have Origin and Access-Control-Request-Method + // headers, the request is outside the scope of CORS + if originHeader == "" || c.Get(fiber.HeaderAccessControlRequestMethod) == "" { return c.Next() } @@ -211,8 +202,9 @@ func New(config ...Config) fiber.Handler { } // Simple request + // Ommit allowMethods and allowHeaders, only used for pre-flight requests if c.Method() != fiber.MethodOptions { - setCORSHeaders(c, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge, cfg) + setCORSHeaders(c, allowOrigin, "", "", exposeHeaders, maxAge, cfg) return c.Next() } @@ -233,14 +225,14 @@ func setCORSHeaders(c *fiber.Ctx, allowOrigin, allowMethods, allowHeaders, expos if cfg.AllowCredentials { // When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*' - if allowOrigin != "*" && allowOrigin != "" { - c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) - c.Set(fiber.HeaderAccessControlAllowCredentials, "true") - } else if allowOrigin == "*" { + if allowOrigin == "*" { c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) log.Warn("[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.") + } else if allowOrigin != "" { + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + c.Set(fiber.HeaderAccessControlAllowCredentials, "true") } - } else if len(allowOrigin) > 0 { + } else if allowOrigin != "" { // For non-credential requests, it's safe to set to '*' or specific origins c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) } diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index ff5cdd7c25..2e0b5c2244 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -35,6 +35,7 @@ func Test_CORS_Negative_MaxAge(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") app.Handler()(ctx) @@ -49,6 +50,7 @@ func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { // Test default GET response headers ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") h(ctx) @@ -59,6 +61,7 @@ func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { // Test default OPTIONS (preflight) response headers ctx = &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") h(ctx) @@ -87,6 +90,7 @@ func Test_CORS_Wildcard(t *testing.T) { ctx.Request.SetRequestURI("/") ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) // Perform request handler(ctx) @@ -101,6 +105,7 @@ func Test_CORS_Wildcard(t *testing.T) { ctx = &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) handler(ctx) utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) @@ -128,6 +133,7 @@ func Test_CORS_Origin_AllowCredentials(t *testing.T) { ctx.Request.SetRequestURI("/") ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) // Perform request handler(ctx) @@ -141,6 +147,7 @@ func Test_CORS_Origin_AllowCredentials(t *testing.T) { // Test non OPTIONS (preflight) response headers ctx = &fasthttp.RequestCtx{} ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.SetMethod(fiber.MethodGet) handler(ctx) @@ -183,7 +190,9 @@ func Test_CORS_Invalid_Origins_Panic(t *testing.T) { "http://foo.[a-z]*.example.com", "http://*", "https://*", + "http://*.com*", "invalid url", + "http://origin.com,invalid url", // add more invalid origins as needed } @@ -226,6 +235,7 @@ func Test_CORS_Subdomain(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") // Perform request @@ -240,6 +250,7 @@ func Test_CORS_Subdomain(t *testing.T) { // Make request with domain only (disallowed) ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") handler(ctx) @@ -252,6 +263,7 @@ func Test_CORS_Subdomain(t *testing.T) { // Make request with allowed origin ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://test.example.com") handler(ctx) @@ -270,6 +282,11 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { reqOrigin: "http://example.com", shouldAllowOrigin: true, }, + { + pattern: "HTTP://EXAMPLE.COM", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, { pattern: "https://example.com", reqOrigin: "https://example.com", @@ -300,6 +317,11 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { reqOrigin: "http://aaa.example.com:8080", shouldAllowOrigin: true, }, + { + pattern: "http://*.example.com", + reqOrigin: "http://1.2.aaa.example.com", + shouldAllowOrigin: true, + }, { pattern: "http://example.com", reqOrigin: "http://gofiber.com", @@ -366,6 +388,7 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, tt.reqOrigin) handler(ctx) @@ -422,6 +445,103 @@ func Test_CORS_Next(t *testing.T) { utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) } +// go test -run Test_CORS_Headers_BasedOnRequestType +func Test_CORS_Headers_BasedOnRequestType(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{})) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + methods := []string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + fiber.MethodHead, + } + + // Get handler pointer + handler := app.Handler() + + t.Run("Without origin and Access-Control-Request-Method headers", func(t *testing.T) { + t.Parallel() + // Make request without origin header, and without Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(method) + ctx.Request.SetRequestURI("https://example.com/") + handler(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should not be set") + } + }) + + t.Run("With origin but without Access-Control-Request-Method header", func(t *testing.T) { + t.Parallel() + // Make request with origin header, but without Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(method) + ctx.Request.SetRequestURI("https://example.com/") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") + handler(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should not be set") + } + }) + + t.Run("Without origin but with Access-Control-Request-Method header", func(t *testing.T) { + t.Parallel() + // Make request without origin header, but with Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(method) + ctx.Request.SetRequestURI("https://example.com/") + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + handler(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should not be set") + } + }) + + t.Run("Preflight request with origin and Access-Control-Request-Method headers", func(t *testing.T) { + t.Parallel() + // Make preflight request with origin header and with Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.SetRequestURI("https://example.com/") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, method) + handler(ctx) + utils.AssertEqual(t, 204, ctx.Response.StatusCode(), "Status code should be 204") + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should be set") + utils.AssertEqual(t, "GET,POST,HEAD,PUT,DELETE,PATCH", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)), "Access-Control-Allow-Methods header should be set (preflight request)") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)), "Access-Control-Allow-Headers header should be set (preflight request)") + } + }) + + t.Run("Non-preflight request with origin and Access-Control-Request-Method headers", func(t *testing.T) { + t.Parallel() + // Make non-preflight request with origin header and with Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(method) + ctx.Request.SetRequestURI("https://example.com/api/action") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, method) + handler(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should be set") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)), "Access-Control-Allow-Methods header should not be set (non-preflight request)") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)), "Access-Control-Allow-Headers header should not be set (non-preflight request)") + } + }) +} + func Test_CORS_AllowOriginsAndAllowOriginsFunc(t *testing.T) { t.Parallel() // New fiber instance @@ -440,6 +560,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") // Perform request @@ -454,6 +575,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc(t *testing.T) { // Make request with allowed origin ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-1.com") handler(ctx) @@ -466,6 +588,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc(t *testing.T) { // Make request with allowed origin ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-2.com") handler(ctx) @@ -505,6 +628,7 @@ func Test_CORS_AllowOriginsFunc(t *testing.T) { // Make request with allowed origin ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-2.com") handler(ctx) @@ -569,7 +693,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { Name: "AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginAllowed", Config: Config{ AllowOrigins: "http://aaa.com", - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return true }, }, @@ -580,7 +704,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { Name: "AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginNotAllowed", Config: Config{ AllowOrigins: "http://aaa.com", - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return true }, }, @@ -591,7 +715,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { Name: "AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginAllowed", Config: Config{ AllowOrigins: "http://aaa.com", - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return false }, }, @@ -602,7 +726,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { Name: "AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginNotAllowed", Config: Config{ AllowOrigins: "http://aaa.com", - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return false }, }, @@ -622,7 +746,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { Name: "AllowOriginsEmpty/AllowOriginsFuncReturnsTrue/OriginAllowed", Config: Config{ AllowOrigins: "", - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return true }, }, @@ -633,7 +757,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { Name: "AllowOriginsEmpty/AllowOriginsFuncReturnsFalse/OriginNotAllowed", Config: Config{ AllowOrigins: "", - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return false }, }, @@ -652,6 +776,7 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin) handler(ctx) @@ -674,7 +799,7 @@ func Test_CORS_AllowCredentials(t *testing.T) { Name: "AllowOriginsFuncDefined", Config: Config{ AllowCredentials: true, - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return true }, }, @@ -687,7 +812,7 @@ func Test_CORS_AllowCredentials(t *testing.T) { Name: "fiber-ghsa-fmg4-x8pw-hjhg-wildcard-credentials", Config: Config{ AllowCredentials: true, - AllowOriginsFunc: func(origin string) bool { + AllowOriginsFunc: func(_ string) bool { return true }, }, @@ -742,6 +867,7 @@ func Test_CORS_AllowCredentials(t *testing.T) { ctx := &fasthttp.RequestCtx{} ctx.Request.SetRequestURI("/") ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin) handler(ctx) diff --git a/middleware/cors/utils_test.go b/middleware/cors/utils_test.go index adc729d05f..47dddc2c69 100644 --- a/middleware/cors/utils_test.go +++ b/middleware/cors/utils_test.go @@ -2,6 +2,8 @@ package cors import ( "testing" + + "github.com/gofiber/fiber/v2/utils" ) // go test -run -v Test_normalizeOrigin @@ -16,6 +18,9 @@ func Test_normalizeOrigin(t *testing.T) { {"http://example.com:3000", true, "http://example.com:3000"}, // Port should be preserved. {"http://example.com:3000/", true, "http://example.com:3000"}, // Trailing slash should be removed. {"http://", false, ""}, // Invalid origin should not be accepted. + {"file:///etc/passwd", false, ""}, // File scheme should not be accepted. + {"https://*example.com", false, ""}, // Wildcard domain should not be accepted. + {"http://*.example.com", false, ""}, // Wildcard subdomain should not be accepted. {"http://example.com/path", false, ""}, // Path should not be accepted. {"http://example.com?query=123", false, ""}, // Query should not be accepted. {"http://example.com#fragment", false, ""}, // Fragment should not be accepted. @@ -105,3 +110,86 @@ func Test_normalizeDomain(t *testing.T) { } } } + +// go test -v -run=^$ -bench=Benchmark_CORS_SubdomainMatch -benchmem -count=4 +func Benchmark_CORS_SubdomainMatch(b *testing.B) { + s := subdomain{ + prefix: "www", + suffix: ".example.com", + } + + o := "www.example.com" + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + s.match(o) + } +} + +func Test_CORS_SubdomainMatch(t *testing.T) { + tests := []struct { + name string + sub subdomain + origin string + expected bool + }{ + { + name: "match with different scheme", + sub: subdomain{prefix: "http://api.", suffix: ".example.com"}, + origin: "https://api.service.example.com", + expected: false, + }, + { + name: "match with different scheme", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "http://api.service.example.com", + expected: false, + }, + { + name: "match with valid subdomain", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "https://api.service.example.com", + expected: true, + }, + { + name: "match with valid nested subdomain", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "https://1.2.api.service.example.com", + expected: true, + }, + + { + name: "no match with invalid prefix", + sub: subdomain{prefix: "https://abc.", suffix: ".example.com"}, + origin: "https://service.example.com", + expected: false, + }, + { + name: "no match with invalid suffix", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "https://api.example.org", + expected: false, + }, + { + name: "no match with empty origin", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "", + expected: false, + }, + { + name: "partial match not considered a match", + sub: subdomain{prefix: "https://service.", suffix: ".example.com"}, + origin: "https://api.example.com", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.sub.match(tt.origin) + utils.AssertEqual(t, tt.expected, got, "subdomain.match()") + }) + } +} From ba10e68d01399fe34e15b84e63164a85c3dab6fb Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Mon, 25 Mar 2024 11:30:20 -0300 Subject: [PATCH 39/46] test(middleware/csrf): Fix Benchmark Tests (#2932) * test(middleware/csrf): fix Benchmark_Middleware_CSRF_* * fix(middleware/csrf): update refererMatchesHost() --- middleware/csrf/csrf.go | 9 +++++---- middleware/csrf/csrf_test.go | 25 +++++++++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index 939daaea79..3c4fdc0529 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -4,6 +4,7 @@ import ( "errors" "net/url" "reflect" + "strings" "time" "github.com/gofiber/fiber/v2" @@ -220,7 +221,7 @@ func isCsrfFromCookie(extractor interface{}) bool { // returns an error if the referer header is not present or is invalid // returns nil if the referer header is valid func refererMatchesHost(c *fiber.Ctx) error { - referer := c.Get(fiber.HeaderReferer) + referer := strings.ToLower(c.Get(fiber.HeaderReferer)) if referer == "" { return ErrNoReferer } @@ -230,9 +231,9 @@ func refererMatchesHost(c *fiber.Ctx) error { return ErrBadReferer } - if refererURL.Scheme+"://"+refererURL.Host != c.Protocol()+"://"+c.Hostname() { - return ErrBadReferer + if refererURL.Scheme == c.Protocol() && refererURL.Host == c.Hostname() { + return nil } - return nil + return ErrBadReferer } diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index a51a932386..60f93abef1 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -992,7 +992,10 @@ func Benchmark_Middleware_CSRF_Check(b *testing.B) { return c.SendStatus(fiber.StatusTeapot) }) - fctx := &fasthttp.RequestCtx{} + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + h := app.Handler() ctx := &fasthttp.RequestCtx{} @@ -1002,17 +1005,27 @@ func Benchmark_Middleware_CSRF_Check(b *testing.B) { token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) token = strings.Split(strings.Split(token, ";")[0], "=")[1] + // Test Correct Referer POST + ctx.Request.Reset() + ctx.Response.Reset() ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + ctx.Request.URI().SetScheme("https") + ctx.Request.URI().SetHost("example.com") + ctx.Request.Header.SetProtocol("https") + ctx.Request.Header.SetHost("example.com") + ctx.Request.Header.Set(fiber.HeaderReferer, "https://example.com") ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - h(fctx) + h(ctx) } - utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) + utils.AssertEqual(b, fiber.StatusTeapot, ctx.Response.Header.StatusCode()) } // go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_GenerateToken -benchmem -count=4 @@ -1024,7 +1037,6 @@ func Benchmark_Middleware_CSRF_GenerateToken(b *testing.B) { return c.SendStatus(fiber.StatusTeapot) }) - fctx := &fasthttp.RequestCtx{} h := app.Handler() ctx := &fasthttp.RequestCtx{} @@ -1034,8 +1046,9 @@ func Benchmark_Middleware_CSRF_GenerateToken(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - h(fctx) + h(ctx) } - utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) + // Ensure the GET request returns a 418 status code + utils.AssertEqual(b, fiber.StatusTeapot, ctx.Response.Header.StatusCode()) } From 43d5091967e17f1d2a4774e8546e3cf5da21fe57 Mon Sep 17 00:00:00 2001 From: RW Date: Mon, 25 Mar 2024 20:26:29 +0100 Subject: [PATCH 40/46] Prepare release v2.52.3 --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 178125b872..8eba9fa66e 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.52.2" +const Version = "2.52.3" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error From e574c0db52ced42fe3cfb126b72c3d88ae5e290b Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 26 Mar 2024 17:57:42 -0300 Subject: [PATCH 41/46] fix(middleware/cors): CORS handling (#2937) * fix(middleware/cors): CORS handling * fix(middleware/cors): Vary header handling * test(middleware/cors): Ensure Vary Headers checked --- middleware/cors/cors.go | 30 ++++++++++--- middleware/cors/cors_test.go | 86 +++++++++++++++++++----------------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index 7debfdfaa0..8fd70b3c60 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -162,9 +162,19 @@ func New(config ...Config) fiber.Handler { // Get originHeader header originHeader := strings.ToLower(c.Get(fiber.HeaderOrigin)) - // If the request does not have Origin and Access-Control-Request-Method - // headers, the request is outside the scope of CORS - if originHeader == "" || c.Get(fiber.HeaderAccessControlRequestMethod) == "" { + // If the request does not have Origin header, the request is outside the scope of CORS + if originHeader == "" { + // See https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches + // Unless all origins are allowed, we include the Vary header to cache the response correctly + if !allowAllOrigins { + c.Vary(fiber.HeaderOrigin) + } + + return c.Next() + } + + // If it's a preflight request and doesn't have Access-Control-Request-Method header, it's outside the scope of CORS + if c.Method() == fiber.MethodOptions && c.Get(fiber.HeaderAccessControlRequestMethod) == "" { return c.Next() } @@ -204,13 +214,23 @@ func New(config ...Config) fiber.Handler { // Simple request // Ommit allowMethods and allowHeaders, only used for pre-flight requests if c.Method() != fiber.MethodOptions { + if !allowAllOrigins { + // See https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches + c.Vary(fiber.HeaderOrigin) + } setCORSHeaders(c, allowOrigin, "", "", exposeHeaders, maxAge, cfg) return c.Next() } - // Preflight request + // Pre-flight request + + // Response to OPTIONS request should not be cached but, + // some caching can be configured to cache such responses. + // To Avoid poisoning the cache, we include the Vary header + // of preflight responses: c.Vary(fiber.HeaderAccessControlRequestMethod) c.Vary(fiber.HeaderAccessControlRequestHeaders) + c.Vary(fiber.HeaderOrigin) setCORSHeaders(c, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge, cfg) @@ -221,8 +241,6 @@ func New(config ...Config) fiber.Handler { // Function to set CORS headers func setCORSHeaders(c *fiber.Ctx, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge string, cfg Config) { - c.Vary(fiber.HeaderOrigin) - if cfg.AllowCredentials { // When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*' if allowOrigin == "*" { diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index 2e0b5c2244..cea2ea5d9b 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -50,7 +50,6 @@ func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { // Test default GET response headers ctx := &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodGet) - ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") h(ctx) @@ -70,6 +69,44 @@ func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) } +func Test_CORS_AllowOrigins_Vary(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New( + Config{ + AllowOrigins: "http://localhost", + }, + )) + + h := app.Handler() + + // Test Vary header non-Cors request + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + utils.AssertEqual(t, true, strings.Contains(string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin), "Vary header should be set for Origin") + + // Test Vary header Cors preflight request + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + h(ctx) + vh := string(ctx.Response.Header.Peek(fiber.HeaderVary)) + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderOrigin), "Vary header should be set for Origin") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestMethod), "Vary header should be set for Access-Control-Request-Method") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestHeaders), "Vary header should be set for Access-Control-Request-Headers") + + // Test Vary header Cors request + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + h(ctx) + utils.AssertEqual(t, true, strings.Contains(string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin), "Vary header should be set for Origin") +} + // go test -run -v Test_CORS_Wildcard func Test_CORS_Wildcard(t *testing.T) { t.Parallel() @@ -97,6 +134,10 @@ func Test_CORS_Wildcard(t *testing.T) { // Check result utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) // Validates request is not reflecting origin in the response + vh := string(ctx.Response.Header.Peek(fiber.HeaderVary)) + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderOrigin), "Vary header should be set for Origin") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestMethod), "Vary header should be set for Access-Control-Request-Method") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestHeaders), "Vary header should be set for Access-Control-Request-Headers") utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) utils.AssertEqual(t, "3600", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) utils.AssertEqual(t, "Authentication", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) @@ -105,9 +146,9 @@ func Test_CORS_Wildcard(t *testing.T) { ctx = &fasthttp.RequestCtx{} ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") - ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) handler(ctx) + utils.AssertEqual(t, false, strings.Contains(string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin), "Vary header should not be set for Origin") utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) utils.AssertEqual(t, "X-Request-ID", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders))) } @@ -147,7 +188,6 @@ func Test_CORS_Origin_AllowCredentials(t *testing.T) { // Test non OPTIONS (preflight) response headers ctx = &fasthttp.RequestCtx{} ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") - ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) ctx.Request.Header.SetMethod(fiber.MethodGet) handler(ctx) @@ -466,7 +506,7 @@ func Test_CORS_Headers_BasedOnRequestType(t *testing.T) { // Get handler pointer handler := app.Handler() - t.Run("Without origin and Access-Control-Request-Method headers", func(t *testing.T) { + t.Run("Without origin", func(t *testing.T) { t.Parallel() // Make request without origin header, and without Access-Control-Request-Method for _, method := range methods { @@ -479,34 +519,6 @@ func Test_CORS_Headers_BasedOnRequestType(t *testing.T) { } }) - t.Run("With origin but without Access-Control-Request-Method header", func(t *testing.T) { - t.Parallel() - // Make request with origin header, but without Access-Control-Request-Method - for _, method := range methods { - ctx := &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(method) - ctx.Request.SetRequestURI("https://example.com/") - ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") - handler(ctx) - utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") - utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should not be set") - } - }) - - t.Run("Without origin but with Access-Control-Request-Method header", func(t *testing.T) { - t.Parallel() - // Make request without origin header, but with Access-Control-Request-Method - for _, method := range methods { - ctx := &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod(method) - ctx.Request.SetRequestURI("https://example.com/") - ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) - handler(ctx) - utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") - utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should not be set") - } - }) - t.Run("Preflight request with origin and Access-Control-Request-Method headers", func(t *testing.T) { t.Parallel() // Make preflight request with origin header and with Access-Control-Request-Method @@ -524,7 +536,7 @@ func Test_CORS_Headers_BasedOnRequestType(t *testing.T) { } }) - t.Run("Non-preflight request with origin and Access-Control-Request-Method headers", func(t *testing.T) { + t.Run("Non-preflight request with origin", func(t *testing.T) { t.Parallel() // Make non-preflight request with origin header and with Access-Control-Request-Method for _, method := range methods { @@ -532,7 +544,6 @@ func Test_CORS_Headers_BasedOnRequestType(t *testing.T) { ctx.Request.Header.SetMethod(method) ctx.Request.SetRequestURI("https://example.com/api/action") ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") - ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, method) handler(ctx) utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should be set") @@ -901,7 +912,6 @@ func Benchmark_CORS_NewHandler(b *testing.B) { req.Header.SetMethod(fiber.MethodGet) req.SetRequestURI("/") req.Header.Set(fiber.HeaderOrigin, "http://localhost") - req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") ctx.Init(req, nil, nil) @@ -942,7 +952,6 @@ func Benchmark_CORS_NewHandlerParallel(b *testing.B) { req.Header.SetMethod(fiber.MethodGet) req.SetRequestURI("/") req.Header.Set(fiber.HeaderOrigin, "http://localhost") - req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") ctx.Init(req, nil, nil) @@ -976,7 +985,6 @@ func Benchmark_CORS_NewHandlerSingleOrigin(b *testing.B) { req.Header.SetMethod(fiber.MethodGet) req.SetRequestURI("/") req.Header.Set(fiber.HeaderOrigin, "http://example.com") - req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") ctx.Init(req, nil, nil) @@ -1017,7 +1025,6 @@ func Benchmark_CORS_NewHandlerSingleOriginParallel(b *testing.B) { req.Header.SetMethod(fiber.MethodGet) req.SetRequestURI("/") req.Header.Set(fiber.HeaderOrigin, "http://example.com") - req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") ctx.Init(req, nil, nil) @@ -1051,7 +1058,6 @@ func Benchmark_CORS_NewHandlerWildcard(b *testing.B) { req.Header.SetMethod(fiber.MethodGet) req.SetRequestURI("/") req.Header.Set(fiber.HeaderOrigin, "http://example.com") - req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") ctx.Init(req, nil, nil) @@ -1092,7 +1098,6 @@ func Benchmark_CORS_NewHandlerWildcardParallel(b *testing.B) { req.Header.SetMethod(fiber.MethodGet) req.SetRequestURI("/") req.Header.Set(fiber.HeaderOrigin, "http://example.com") - req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") ctx.Init(req, nil, nil) @@ -1122,6 +1127,7 @@ func Benchmark_CORS_NewHandlerPreflight(b *testing.B) { h := app.Handler() ctx := &fasthttp.RequestCtx{} + // Preflight request req := &fasthttp.Request{} req.Header.SetMethod(fiber.MethodOptions) req.SetRequestURI("/") From a6f4c133bc749696b8d1f551bc56d48efda138d3 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 26 Mar 2024 18:22:42 -0300 Subject: [PATCH 42/46] fix(middleware/cors): Vary header handling non-cors OPTIONS requests (#2939) * fix(middleware/cors): Vary header handling non-cors OPTIONS requests * chore(middleware/cors): Add Vary header for non-CORS OPTIONS requests comment --- middleware/cors/cors.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index 8fd70b3c60..f38da1bb71 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -175,6 +175,11 @@ func New(config ...Config) fiber.Handler { // If it's a preflight request and doesn't have Access-Control-Request-Method header, it's outside the scope of CORS if c.Method() == fiber.MethodOptions && c.Get(fiber.HeaderAccessControlRequestMethod) == "" { + // Response to OPTIONS request should not be cached but, + // some caching can be configured to cache such responses. + // To Avoid poisoning the cache, we include the Vary header + // for non-CORS OPTIONS requests: + c.Vary(fiber.HeaderOrigin) return c.Next() } From fd811cf84af282db8ec50adedce01a5886d5fd46 Mon Sep 17 00:00:00 2001 From: RW Date: Tue, 26 Mar 2024 22:40:09 +0100 Subject: [PATCH 43/46] prepare release v2.52.4 --- app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.go b/app.go index 8eba9fa66e..2568e06dd8 100644 --- a/app.go +++ b/app.go @@ -30,7 +30,7 @@ import ( ) // Version of current fiber package -const Version = "2.52.3" +const Version = "2.52.4" // Handler defines a function to serve HTTP requests. type Handler = func(*Ctx) error From 08db30e8716c999857f76774dcc95284963d8765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 28 Mar 2024 10:01:34 +0100 Subject: [PATCH 44/46] merge v2 in main(v3) --- middleware/adaptor/adaptor_test.go | 2 -- middleware/logger/logger_test.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index e20ce12a1d..8e3de130c7 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -245,8 +245,6 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A require.Equal(t, expectedHost, string(c.Request().Header.Host()), "Host") require.Equal(t, "http://"+expectedHost, c.BaseURL(), "BaseURL") require.Equal(t, expectedHost, string(c.Request().Header.Host()), "Host") - utils.AssertEqual(t, "http://"+expectedHost, c.BaseURL(), "BaseURL") - utils.AssertEqual(t, expectedRemoteAddr, c.Context().RemoteAddr().String(), "RemoteAddr") body := string(c.Body()) require.Equal(t, expectedBody, body, "Body") diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index f99ac5ef4e..0aa517bcf5 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -149,7 +149,7 @@ type fakeErrorOutput int func (o *fakeErrorOutput) Write([]byte) (int, error) { *o++ - return 0, nil + return 0, errors.New("fake output") } // go test -run Test_Logger_ErrorOutput_WithoutColor From a91045183fa65f012a7e95b83002636618c1a33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 28 Mar 2024 10:07:01 +0100 Subject: [PATCH 45/46] merge v2 in main(v3) --- .github/scripts/sync_docs.sh | 19 +++++++++---------- .github/workflows/benchmark.yml | 3 ++- .github/workflows/codeql-analysis.yml | 3 ++- .github/workflows/linter.yml | 3 ++- .github/workflows/release-drafter.yml | 3 ++- .github/workflows/sync-docs.yml | 6 +++--- .github/workflows/test.yml | 3 ++- .github/workflows/vulncheck.yml | 3 ++- middleware/adaptor/adaptor_test.go | 1 - 9 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.github/scripts/sync_docs.sh b/.github/scripts/sync_docs.sh index 1bdb80a8b7..4898039680 100755 --- a/.github/scripts/sync_docs.sh +++ b/.github/scripts/sync_docs.sh @@ -18,16 +18,15 @@ git config --global user.name "${AUTHOR_USERNAME}" git clone https://${TOKEN}@${REPO_URL} fiber-docs # Handle push event -#if [ "$EVENT" == "push" ]; then -# latest_commit=$(git rev-parse --short HEAD) -# log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/) -# if [[ $log_output != "" ]]; then -# cp -a docs/* fiber-docs/docs/${REPO_DIR} -# fi -# -## Handle release event -#el -if [ "$EVENT" == "release" ]; then +if [ "$EVENT" == "push" ]; then + latest_commit=$(git rev-parse --short HEAD) + log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/) + if [[ $log_output != "" ]]; then + cp -a docs/* fiber-docs/docs/${REPO_DIR} + fi + +# Handle release event +elif [ "$EVENT" == "release" ]; then major_version="${TAG_NAME%%.*}" # Form new version name diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 6f8f9b72c2..6b2ad36b69 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,7 +1,8 @@ on: push: branches: - - v2 + - master + - main paths: - "**" - "!docs/**" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0476abfba2..82036cc967 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,7 +3,8 @@ name: "CodeQL" on: push: branches: - - v2 + - master + - main paths: - "**" - "!docs/**" diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index c030d6d31c..10c05f26b1 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -4,7 +4,8 @@ name: golangci-lint on: push: branches: - - v2 + - master + - main pull_request: permissions: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 898049c404..601dd98937 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -4,7 +4,8 @@ on: push: # branches to consider in the event; optional, defaults to all branches: - - v2 + - master + - main jobs: update_release_draft: diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml index 4016883fda..eb35d75bd2 100644 --- a/.github/workflows/sync-docs.yml +++ b/.github/workflows/sync-docs.yml @@ -3,7 +3,8 @@ name: "Sync docs" on: push: branches: - - v2 + - master + - main paths: - "docs/**" release: @@ -11,13 +12,12 @@ on: jobs: sync-docs: - if: false runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event.release.tag_name }} + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 2 - name: Setup Node.js environment diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f97bcfe0c..8fff9b80b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,8 @@ on: push: branches: - - v2 + - master + - main paths: - "**" - "!docs/**" diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index f28ab5b0e2..e6d29ccb4b 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -3,7 +3,8 @@ name: Run govulncheck on: push: branches: - - v2 + - master + - main paths: - "**" - "!docs/**" diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index 8e3de130c7..677af799fd 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -244,7 +244,6 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A require.Equal(t, expectedHost, c.Hostname(), "Host") require.Equal(t, expectedHost, string(c.Request().Header.Host()), "Host") require.Equal(t, "http://"+expectedHost, c.BaseURL(), "BaseURL") - require.Equal(t, expectedHost, string(c.Request().Header.Host()), "Host") body := string(c.Body()) require.Equal(t, expectedBody, body, "Body") From 2f7d83711c0531fcac26b7585a81e1843dd65361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 28 Mar 2024 10:07:33 +0100 Subject: [PATCH 46/46] merge v2 in main(v3) --- middleware/adaptor/adaptor_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index 677af799fd..a14ea60669 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -244,6 +244,7 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A require.Equal(t, expectedHost, c.Hostname(), "Host") require.Equal(t, expectedHost, string(c.Request().Header.Host()), "Host") require.Equal(t, "http://"+expectedHost, c.BaseURL(), "BaseURL") + require.Equal(t, expectedRemoteAddr, c.Context().RemoteAddr().String(), "RemoteAddr") body := string(c.Body()) require.Equal(t, expectedBody, body, "Body")