From e70b2e28d66473ab6ad0d69f788dd2a6009c247e Mon Sep 17 00:00:00 2001 From: joey1123455 <112909075+joey1123455@users.noreply.github.com> Date: Thu, 12 Oct 2023 09:44:15 +0100 Subject: [PATCH] Cookie parser (#2656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * prep for branching * feature: added a cookie parser and tests appropriate tests * :sparkles: feature: added a cookie parser and appropriate tests * made correction to docs * linted using gofumpt * ctx_test linted, cookieParser schema added * fix lint errors (Cookie parser #2656) * removed extra lines, tested return values --------- Co-authored-by: René Werner --- ctx.go | 41 +++++++++++++- ctx_test.go | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/api/ctx.md | 32 +++++++++++ 3 files changed, 216 insertions(+), 3 deletions(-) diff --git a/ctx.go b/ctx.go index 3d19a45fbf..a5ba85f947 100644 --- a/ctx.go +++ b/ctx.go @@ -38,22 +38,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() { @@ -502,6 +503,40 @@ 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 + + // loop through all cookies + 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). diff --git a/ctx_test.go b/ctx_test.go index d8edea0e9e..ef9007293f 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -928,6 +928,152 @@ 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 Test_Ctx_CookieParserSchema -v +func Test_Ctx_CookieParser_Schema(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type result struct { + Maths int `cookie:"maths"` + English int `cookie:"english"` + } + type resStruct struct { + Name string `cookie:"name"` + Age int `cookie:"age"` + Result result `cookie:"result"` + } + res := &resStruct{ + Name: "Joseph", + Age: 10, + Result: result{ + Maths: 10, + English: 10, + }, + } + + // set cookie + c.Request().Header.Set("Cookie", "name=Joseph") + c.Request().Header.Set("Cookie", "age=10") + c.Request().Header.Set("Cookie", "result.maths=10") + c.Request().Header.Set("Cookie", "result.english=10") + hR := new(resStruct) + r := c.CookieParser(hR) + + utils.AssertEqual(t, nil, r) + utils.AssertEqual(t, *res, *hR) +} + +// 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") + + var err error + // Run the function b.N times + for i := 0; i < b.N; i++ { + err = c.CookieParser(cookie1) + } + utils.AssertEqual(b, nil, err) +} + // go test -run Test_Ctx_Cookies func Test_Ctx_Cookies(t *testing.T) { t.Parallel() diff --git a/docs/api/ctx.md b/docs/api/ctx.md index b1e14f2d6d..1729438d7f 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -403,6 +403,38 @@ 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.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.