Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookie parser #2656

Merged
merged 9 commits into from
Oct 12, 2023
40 changes: 37 additions & 3 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,23 @@ const (
// maxParams defines the maximum number of parameters per route.
const maxParams = 30

// Some constants for BodyParser, QueryParser and ReqHeaderParser.
// Some constants for BodyParser, QueryParser, CookieParser and ReqHeaderParser.
const (
queryTag = "query"
reqHeaderTag = "reqHeader"
bodyTag = "form"
paramsTag = "params"
cookieTag = "cookie"
)

// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx
const userContextKey = "__local_user_context__"

var (
// decoderPoolMap helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance
// decoderPoolMap helps to improve BodyParser's, QueryParser's, CookieParser's and ReqHeaderParser's performance
decoderPoolMap = map[string]*sync.Pool{}
// tags is used to classify parser's pool
tags = []string{queryTag, bodyTag, reqHeaderTag, paramsTag}
tags = []string{queryTag, bodyTag, reqHeaderTag, paramsTag, cookieTag}
)

func init() {
Expand Down Expand Up @@ -503,6 +504,39 @@ func (c *Ctx) Cookies(key string, defaultValue ...string) string {
return defaultString(c.app.getString(c.fasthttp.Request.Header.Cookie(key)), defaultValue)
}

// CookieParser is used to bind cookies to a struct
func (c *Ctx) CookieParser(out interface{}) error {
data := make(map[string][]string)
var err error

c.fasthttp.Request.Header.VisitAllCookie(func(key, val []byte) {
if err != nil {
return
}

k := c.app.getString(key)
v := c.app.getString(val)

if strings.Contains(k, "[") {
k, err = parseParamSquareBrackets(k)
}

if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k, cookieTag) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}
})
if err != nil {
return err
}

return c.parseToStruct(cookieTag, out, data)
}

// Download transfers the file from path as an attachment.
// Typically, browsers will prompt the user for download.
// By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog).
Expand Down
109 changes: 109 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,115 @@ func Benchmark_Ctx_Cookie(b *testing.B) {
utils.AssertEqual(b, "John=Doe; path=/; SameSite=Lax", app.getString(c.Response().Header.Peek("Set-Cookie")))
}

// go test -run Test_Ctx_CookieParser -v
func Test_Ctx_CookieParser(t *testing.T) {
t.Parallel()
app := New(Config{EnableSplittingOnParsers: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Cookie struct {
Name string
Class int
Courses []string
}
c.Request().Header.Set("Cookie", "name=doe")
c.Request().Header.Set("Cookie", "class=100")
c.Request().Header.Set("Cookie", "courses=maths,english")
cookie := new(Cookie)

// correct test cases
utils.AssertEqual(t, nil, c.CookieParser(cookie))
utils.AssertEqual(t, "doe", cookie.Name)
utils.AssertEqual(t, 100, cookie.Class)
utils.AssertEqual(t, 2, len(cookie.Courses))

// wrong test cases
empty := new(Cookie)
c.Request().Header.Set("Cookie", "name")
c.Request().Header.Set("Cookie", "class")
c.Request().Header.Set("Cookie", "courses")
utils.AssertEqual(t, nil, c.CookieParser(cookie))
utils.AssertEqual(t, "", empty.Name)
utils.AssertEqual(t, 0, empty.Class)
utils.AssertEqual(t, 0, len(empty.Courses))
}

// go test - run Test_Ctx_CookieParserUsingTag -v
func Test_Ctx_CookieParserUsingTag(t *testing.T) {
t.Parallel()
app := New(Config{EnableSplittingOnParsers: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Cook struct {
ID int `cookie:"id"`
Name string `cookie:"name"`
Courses []string `cookie:"courses"`
Enrolled bool `cookie:"student"`
Fees float32 `cookie:"fee"`
Grades []uint8 `cookie:"score"`
}
cookie1 := new(Cook)
cookie1.Name = "Joseph"
utils.AssertEqual(t, "Joseph", cookie1.Name)

c.Request().Header.Set("Cookie", "id=1")
c.Request().Header.Set("Cookie", "name=Joey")
c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics")
c.Request().Header.Set("Cookie", "student=true")
c.Request().Header.Set("Cookie", "fee=45.78")
c.Request().Header.Set("Cookie", "score=7,6,10")
utils.AssertEqual(t, nil, c.CookieParser(cookie1))
utils.AssertEqual(t, "Joey", cookie1.Name)
utils.AssertEqual(t, true, cookie1.Enrolled)
utils.AssertEqual(t, float32(45.78), cookie1.Fees)
utils.AssertEqual(t, []uint8{7, 6, 10}, cookie1.Grades)

type RequiredCookie struct {
House string `cookie:"house,required"`
}
rc := new(RequiredCookie)
utils.AssertEqual(t, "failed to decode: house is empty", c.CookieParser(rc).Error())

type ArrayCookie struct {
Dates []int
}

ac := new(ArrayCookie)
c.Request().Header.Set("Cookie", "dates[]=7,6,10")
utils.AssertEqual(t, nil, c.CookieParser(ac))
utils.AssertEqual(t, 3, len(ac.Dates))

}

// go test - run Benchmark_Ctx_CookieParser -v
func Benchmark_Ctx_CookieParser(b *testing.B) {
app := New(Config{EnableSplittingOnParsers: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Cook struct {
ID int `cookie:"id"`
Name string `cookie:"name"`
Courses []string `cookie:"courses"`
Enrolled bool `cookie:"student"`
Fees float32 `cookie:"fee"`
Grades []uint8 `cookie:"score"`
}
cookie1 := new(Cook)
cookie1.Name = "Joseph"

c.Request().Header.Set("Cookie", "id=1")
c.Request().Header.Set("Cookie", "name=Joey")
c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics")
c.Request().Header.Set("Cookie", "student=true")
c.Request().Header.Set("Cookie", "fee=45.78")
c.Request().Header.Set("Cookie", "score=7,6,10")

// Run the function b.N times
for i := 0; i < b.N; i++ {
_ = c.CookieParser(cookie1)
}
}

// go test -run Test_Ctx_Cookies
func Test_Ctx_Cookies(t *testing.T) {
t.Parallel()
Expand Down
38 changes: 38 additions & 0 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,44 @@ app.Get("/", func(c *fiber.Ctx) error {
})
```

## CookieParser

This method is similar to [BodyParser](ctx.md#bodyparser), but for cookie parameters.
It is important to use the struct tag "cookie". For example, if you want to parse a cookie with a field called Age, you would use a struct field of `cookie:"age"`.

```go title="Signature"
func (c *Ctx) CookieParser(out interface{}) error
```

```go title="Example"
// Field names should start with an uppercase letter
type Person struct {
Name string `cookie:"name"`
Age int `cookie:"age"`
Job bool `cookie:"job"`
}

app.Get("/", func(c *fiber.Ctx) error {
p := new(Person)

if err := c.CookieParser(p); err != nil {
return err
}

log.Println(p.Name) // Joseph
log.Println(p.Age) // 23
log.Println(p.Job) // true

// ...
})
// Run tests with the following curl command

// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat"
// curl.exe --cookie "name=Joseph; age=23; job=true" http://localhost:8000/
```



## Cookies

Get cookie value by key, you could pass an optional default value that will be returned if the cookie key does not exist.
Expand Down
Loading