Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 38 additions & 13 deletions cmd/internal/migrations/v3/middleware_locals.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,54 @@ import (

func MigrateMiddlewareLocals(cmd *cobra.Command, cwd string, _, _ *semver.Version) error {
changed, err := internal.ChangeFileContent(cwd, func(content string) string {
replacements := []struct {
re *regexp.Regexp
repl string
ctxMap := map[string]string{
"requestid": "requestid.FromContext(%s)",
"csrf": "csrf.TokenFromContext(%s)",
"csrf_handler": "csrf.HandlerFromContext(%s)",
"session": "session.FromContext(%s)",
"username": "basicauth.UsernameFromContext(%s)",
"password": "basicauth.PasswordFromContext(%s)",
"token": "keyauth.TokenFromContext(%s)",
}

Comment on lines +15 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Default "token" mapping can collide with extractor-derived mappings

Seeding ctxMap with "token" -> keyauth.TokenFromContext(%s) creates a global mapping that may be overwritten (or overwrite) extractor-derived mappings when multiple middlewares reuse "token" (e.g., both keyauth and csrf set ContextKey to "token"). Today, precedence is purely order-of-extractor, which can silently misroute replacements across the file.

Recommend: remove the eager default for "token" and add it as a fallback only if no extractor populated that key. This preserves the common keyauth case while avoiding accidental override when another middleware purposely uses "token".

Apply:

@@
-        ctxMap := map[string]string{
+        ctxMap := map[string]string{
             "requestid":    "requestid.FromContext(%s)",
             "csrf":         "csrf.TokenFromContext(%s)",
             "csrf_handler": "csrf.HandlerFromContext(%s)",
             "session":      "session.FromContext(%s)",
             "username":     "basicauth.UsernameFromContext(%s)",
             "password":     "basicauth.PasswordFromContext(%s)",
-            "token":        "keyauth.TokenFromContext(%s)",
         }
@@
-        for _, e := range extractors {
+        for _, e := range extractors {
             re := regexp.MustCompile(e.pkg + `\.Config{[^}]*` + e.field + `:\s*"([^"]+)"`)
             matches := re.FindAllStringSubmatch(content, -1)
             for _, m := range matches {
                 ctxMap[m[1]] = e.replFmt
             }
         }
+
+        // Provide a sensible default mapping for token -> keyauth only if no extractor populated it.
+        if _, ok := ctxMap["token"]; !ok {
+            ctxMap["token"] = "keyauth.TokenFromContext(%s)"
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ctxMap := map[string]string{
"requestid": "requestid.FromContext(%s)",
"csrf": "csrf.TokenFromContext(%s)",
"csrf_handler": "csrf.HandlerFromContext(%s)",
"session": "session.FromContext(%s)",
"username": "basicauth.UsernameFromContext(%s)",
"password": "basicauth.PasswordFromContext(%s)",
"token": "keyauth.TokenFromContext(%s)",
}
ctxMap := map[string]string{
"requestid": "requestid.FromContext(%s)",
"csrf": "csrf.TokenFromContext(%s)",
"csrf_handler": "csrf.HandlerFromContext(%s)",
"session": "session.FromContext(%s)",
"username": "basicauth.UsernameFromContext(%s)",
"password": "basicauth.PasswordFromContext(%s)",
}
for _, e := range extractors {
re := regexp.MustCompile(e.pkg + `\.Config{[^}]*` + e.field + `:\s*"([^"]+)"`)
matches := re.FindAllStringSubmatch(content, -1)
for _, m := range matches {
ctxMap[m[1]] = e.replFmt
}
}
// Provide a sensible default mapping for token -> keyauth only if no extractor populated it.
if _, ok := ctxMap["token"]; !ok {
ctxMap["token"] = "keyauth.TokenFromContext(%s)"
}
🤖 Prompt for AI Agents
In cmd/internal/migrations/v3/middleware_locals.go around lines 15 to 24, the
ctxMap pre-populates "token" -> keyauth.TokenFromContext(%s) which can collide
with extractor-derived mappings; remove the eager "token" entry from the initial
ctxMap, and instead add logic after extractor population that checks if "token"
is still unset and only then inserts the keyauth.TokenFromContext fallback;
ensure the new check runs after all extractors are processed so
extractor-provided "token" mappings are preserved and keyauth only provides a
default when no other mapping exists.

extractors := []struct {
pkg string
field string
replFmt string
}{
{regexp.MustCompile(`(\w+)\.Locals\("requestid"\)`), `requestid.FromContext($1)`},
{regexp.MustCompile(`(\w+)\.Locals\("csrf"\)`), `csrf.TokenFromContext($1)`},
{regexp.MustCompile(`(\w+)\.Locals\("csrf_handler"\)`), `csrf.HandlerFromContext($1)`},
{regexp.MustCompile(`(\w+)\.Locals\("session"\)`), `session.FromContext($1)`},
{regexp.MustCompile(`(\w+)\.Locals\("username"\)`), `basicauth.UsernameFromContext($1)`},
{regexp.MustCompile(`(\w+)\.Locals\("password"\)`), `basicauth.PasswordFromContext($1)`},
{regexp.MustCompile(`(\w+)\.Locals\("token"\)`), `keyauth.TokenFromContext($1)`},
{"csrf", "ContextKey", "csrf.TokenFromContext(%s)"},
{"keyauth", "ContextKey", "keyauth.TokenFromContext(%s)"},
{"session", "ContextKey", "session.FromContext(%s)"},
{"basicauth", "ContextUsername", "basicauth.UsernameFromContext(%s)"},
{"basicauth", "ContextPassword", "basicauth.PasswordFromContext(%s)"},
}
for _, r := range replacements {
content = r.re.ReplaceAllString(content, r.repl)

for _, e := range extractors {
re := regexp.MustCompile(e.pkg + `\.Config{[^}]*` + e.field + `:\s*"([^"]+)"`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The regular expression used here is not robust enough for parsing Go source code. The [^}]* pattern will fail to match correctly if the Config struct literal contains any nested {} blocks, which is common with function literals (e.g., for a SuccessHandler field). This will cause the migration to silently miss custom context keys in such configurations.

For example, the regex would fail on this valid code:

_ = csrf.New(csrf.Config{
    SuccessHandler: func(c *fiber.Ctx) error {
        return c.Next()
    },
    ContextKey: "my_csrf_token",
})

Using regular expressions to parse source code is error-prone. A much more reliable approach would be to use the go/parser and go/ast packages to build an Abstract Syntax Tree and inspect it. While more involved, it's the correct way to handle this and would make the migration tool significantly more robust.

matches := re.FindAllStringSubmatch(content, -1)
for _, m := range matches {
ctxMap[m[1]] = e.replFmt
}
}

reLocals := regexp.MustCompile(`(\w+)\.Locals\("([^"]+)"\)`)
content = reLocals.ReplaceAllStringFunc(content, func(s string) string {
sub := reLocals.FindStringSubmatch(s)
ctx := sub[1]
key := sub[2]
if fmtStr, ok := ctxMap[key]; ok {
return fmt.Sprintf(fmtStr, ctx)
}
return s
})

reTypeAssert := regexp.MustCompile(`([\w\.]+FromContext\([^\)]+\))\.\([^\)]+\)`)
content = reTypeAssert.ReplaceAllString(content, "$1")

reComma := regexp.MustCompile(`(\w+)\s*,\s*\w+\s*:=\s*([\w\.]+FromContext\([^\)]+\))`)
content = reComma.ReplaceAllString(content, "$1 := $2")

reCtxKey := regexp.MustCompile(`\s*ContextKey:\s*[^,}\n]+,?`)
reCtxKey := regexp.MustCompile(`\s*Context(?:Username|Password|Key):\s*[^,}\n]+,?`)
content = reCtxKey.ReplaceAllString(content, "")

return content
Expand Down
30 changes: 30 additions & 0 deletions cmd/internal/migrations/v3/middleware_locals_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,33 @@ func main() {
content := readFile(t, file)
assert.NotContains(t, content, "ContextKey")
}

func Test_MigrateMiddlewareLocals_CustomContextKey(t *testing.T) {
t.Parallel()

dir, err := os.MkdirTemp("", "mcustomctx")
require.NoError(t, err)
defer func() { require.NoError(t, os.RemoveAll(dir)) }()

file := writeTempFile(t, dir, `package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/csrf"
)

var _ = csrf.New(csrf.Config{ContextKey: "token"})

func handler(c fiber.Ctx) error {
token := c.Locals("token").(string)
_ = token
return nil
}`)

var buf bytes.Buffer
cmd := newCmd(&buf)
require.NoError(t, v3.MigrateMiddlewareLocals(cmd, dir, nil, nil))

content := readFile(t, file)
assert.Contains(t, content, `token := csrf.TokenFromContext(c)`)
assert.NotContains(t, content, "ContextKey")
}
6 changes: 6 additions & 0 deletions cmd/migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func Test_Migrate_V2_to_V3(t *testing.T) {
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/monitor"
"github.com/gofiber/fiber/v2/middleware/csrf"
)

func handler(c *fiber.Ctx) error {
Expand All @@ -50,7 +51,9 @@ func handler(c *fiber.Ctx) error {
ctx := c.Context()
uc := c.UserContext()
c.SetUserContext(uc)
csrfToken := c.Locals("token").(string)
_ = ctx
_ = csrfToken
return c.Bind(fiber.Map{})
}

Expand All @@ -60,6 +63,7 @@ func main() {
Prefork: true,
Network: "tcp",
})
app.Use(csrf.New(csrf.Config{ContextKey: "token"}))
app.Static("/", "./public")
app.Add(fiber.MethodGet, "/foo", handler)
app.Mount("/api", app)
Expand Down Expand Up @@ -92,6 +96,8 @@ func main() {
at.Contains(content, ".Redirect().To(\"/foo\")")
at.Contains(content, ".Redirect().Back()")
at.Contains(content, "fiber.Params[int](c, \"id\"")
at.Contains(content, "csrf.TokenFromContext(c)")
at.NotContains(content, "ContextKey")
at.Contains(content, ".Use(\"/api\", app)")
at.Contains(content, ".Listen(")
at.Contains(content, "MIMETextJavaScript")
Expand Down
Loading