From d51c0b7b504c25edeec03116270c27b31e0a34f4 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 18 Mar 2017 12:22:20 +0200 Subject: [PATCH] Implement feature request: http://support.iris-go.com/d/29-mark-cookie-for-session-as-secure Example: app := iris.New() app.Adapt(httprouter.New()) // IMPORTANT cookieName := "mycustomsessionid" // AES only supports key sizes of 16, 24 or 32 bytes. // You either need to provide exactly that amount or you derive the key from what you type in. hashKey := []byte("the-big-and-secret-fash-key-here") blockKey := []byte("lot-secret-of-characters-big-too") secureCookie := securecookie.New(hashKey, blockKey) app.Adapt(sessions.New(sessions.Config{ Cookie: cookieName, Encode: secureCookie.Encode, Decode: secureCookie.Decode, })) Former-commit-id: 6fe5ce6cb834d55862242e08405fad4e721caa5b --- adaptors/sessions/config.go | 23 ++++++++++ adaptors/sessions/sessions.go | 45 ++++++++++++++++++ context.go | 28 ++++++++++++ iris.go | 43 +++++++++++++----- policy_sessions_test.go | 86 +++++++++++++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 12 deletions(-) diff --git a/adaptors/sessions/config.go b/adaptors/sessions/config.go index 55e5ef7f..b6911ee5 100644 --- a/adaptors/sessions/config.go +++ b/adaptors/sessions/config.go @@ -31,6 +31,29 @@ type ( // Defaults to false DecodeCookie bool + // Encode the cookie value if not nil. + // Should accept as first argument the cookie name (config.Name) + // as second argument the server's generated session id. + // Should return the new session id, if error the session id setted to empty which is invalid. + // + // Note: Errors are not printed, so you have to know what you're doing, + // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. + // You either need to provide exactly that amount or you derive the key from what you type in. + // + // Defaults to nil + Encode func(cookieName string, value interface{}) (string, error) + // Decode the cookie value if not nil. + // Should accept as first argument the cookie name (config.Name) + // as second second accepts the client's cookie value (the encoded session id). + // Should return an error if decode operation failed. + // + // Note: Errors are not printed, so you have to know what you're doing, + // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. + // You either need to provide exactly that amount or you derive the key from what you type in. + // + // Defaults to nil + Decode func(cookieName string, cookieValue string, v interface{}) error + // Expires the duration of which the cookie must expires (created_time.Add(Expires)). // If you want to delete the cookie when the browser closes, set it to -1. // diff --git a/adaptors/sessions/sessions.go b/adaptors/sessions/sessions.go index 18efdc58..1ab08623 100644 --- a/adaptors/sessions/sessions.go +++ b/adaptors/sessions/sessions.go @@ -90,13 +90,16 @@ func (s *sessions) Start(res http.ResponseWriter, req *http.Request) iris.Sessio var sess iris.Session cookieValue := GetCookie(s.config.Cookie, req) + if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie sid := SessionIDGenerator(s.config.CookieLength) + sess = s.provider.Init(sid, s.config.Expires) cookie := &http.Cookie{} // The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding cookie.Name = s.config.Cookie + cookie.Value = sid cookie.Path = "/" if !s.config.DisableSubdomainPersistence { @@ -143,8 +146,34 @@ func (s *sessions) Start(res http.ResponseWriter, req *http.Request) iris.Sessio cookie.MaxAge = int(cookie.Expires.Sub(time.Now()).Seconds()) } + { + // encode the session id cookie client value right before send it. + if encode := s.config.Encode; encode != nil { + newVal, err := encode(s.config.Cookie, cookie.Value) + if err == nil { + cookie.Value = newVal + } else { + cookie.Value = "" + } + } + } + AddCookie(cookie, res) } else { + + { + // decode the cookie value from the client's cookie right before read the session data. + var cookieValueDecoded *string + if decode := s.config.Decode; decode != nil { + err := decode(s.config.Cookie, cookieValue, &cookieValueDecoded) + if err == nil { + cookieValue = *cookieValueDecoded + } else { + cookieValue = "" + } + } + } + sess = s.provider.Read(cookieValue, s.config.Expires) } return sess @@ -157,6 +186,22 @@ func (s *sessions) Destroy(res http.ResponseWriter, req *http.Request) { return } RemoveCookie(s.config.Cookie, res, req) + + { + // decode the client's cookie value in order to find the server's session id + // to destroy the session data. + var cookieValueDecoded *string + if decode := s.config.Decode; decode != nil { + err := decode(s.config.Cookie, cookieValue, &cookieValueDecoded) + if err == nil { + cookieValue = *cookieValueDecoded + } else { + cookieValue = "" + } + } + + } + s.provider.Destroy(cookieValue) } diff --git a/context.go b/context.go index d90b5b01..63815490 100644 --- a/context.go +++ b/context.go @@ -269,6 +269,34 @@ func (ctx *Context) GetHandlerName() string { return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name() } +// ParamValidate receives a compiled Regexp and execute a parameter's value +// against this regexp, returns true if matched or param not found, otherwise false. +// +// It accepts a compiled regexp to reduce the performance cost on serve time. +// If you need a more automative solution, use the `app.Regex` or `app.RegexSingle` instead. +// +// This function helper is ridiculous simple but it's good to have it on one place. +func (ctx *Context) ParamValidate(compiledExpr *regexp.Regexp, paramName string) bool { + pathPart := ctx.Param(paramName) + if pathPart == "" { + // take care, the router already + // does the param validations + // so if it's empty here it means that + // the router has label it as optional. + // so we skip the check. + return true + } + + if pathPart[0] == '/' { + // it's probably wildcard, we 'should' remove that part to do the matching below + pathPart = pathPart[1:] + } + + // the improtant thing: + // if the path part didn't match with the relative exp, then fire status not found. + return compiledExpr.MatchString(pathPart) +} + // ExecRoute calls any route (mostly "offline" route) like it was requested by the user, but it is not. // Offline means that the route is registered to the iris and have all features that a normal route has // BUT it isn't available by browsing, its handlers executed only when other handler's context call them diff --git a/iris.go b/iris.go index bd311c0d..55ecb923 100644 --- a/iris.go +++ b/iris.go @@ -814,6 +814,7 @@ func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc { ctx.EmitError(StatusInternalServerError) } + // just to check if router is adapted. wp := s.policies.RouterReversionPolicy.WildcardPath if wp == nil { s.Log(ProdMode, "regex cannot be used when a router policy is missing\n"+errRouterIsMissing.Format(s.Config.VHost).Error()) @@ -826,6 +827,8 @@ func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc { "paramName2, expression2. The len should be %2==0") return srvErr } + + // we do compile first to reduce the performance cost at serve time. pairs := make(map[string]*regexp.Regexp, len(pairParamExpr)/2) for i := 0; i < len(pairParamExpr)-1; i++ { @@ -843,18 +846,7 @@ func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc { // return the middleware return func(ctx *Context) { for k, v := range pairs { - pathPart := ctx.Param(k) - if pathPart == "" { - // take care, the router already - // does the param validations - // so if it's empty here it means that - // the router has label it as optional. - // so we skip it, and continue to the next. - continue - } - // the improtant thing: - // if the path part didn't match with the relative exp, then fire status not found. - if !v.MatchString(pathPart) { + if !ctx.ParamValidate(v, k) { ctx.EmitError(StatusNotFound) return } @@ -864,6 +856,33 @@ func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc { } } +func (s *Framework) RegexSingle(paramName string, expr string, onFail HandlerFunc) HandlerFunc { + + // just to check if router is adapted. + wp := s.policies.RouterReversionPolicy.WildcardPath + if wp == nil { + s.Log(ProdMode, "regex cannot be used when a router policy is missing\n"+errRouterIsMissing.Format(s.Config.VHost).Error()) + return onFail + } + + // we do compile first to reduce the performance cost at serve time. + r, err := regexp.Compile(expr) + if err != nil { + s.Log(ProdMode, "regex '"+expr+"' failed. Trace: "+err.Error()) + return onFail + } + + // return the middleware + return func(ctx *Context) { + if !ctx.ParamValidate(r, paramName) { + onFail(ctx) + return + } + // otherwise continue to the next handler... + ctx.Next() + } +} + // RouteParam returns a named parameter as each router defines named path parameters. // For example, with the httprouter(: as named param symbol): // userid should return :userid. diff --git a/policy_sessions_test.go b/policy_sessions_test.go index 545270c4..2916017b 100644 --- a/policy_sessions_test.go +++ b/policy_sessions_test.go @@ -3,6 +3,7 @@ package iris_test import ( "testing" + "github.com/gorilla/securecookie" // optional, to set sessions'' Encode and Decode "gopkg.in/kataras/iris.v6" "gopkg.in/kataras/iris.v6/adaptors/httprouter" "gopkg.in/kataras/iris.v6/adaptors/sessions" @@ -167,3 +168,88 @@ func TestFlashMessages(t *testing.T) { // e.GET("/get/").Expect().Status(http.StatusOK).JSON().Object().Equal(values) e.GET("/get_single").Expect().Status(iris.StatusOK).Body().Equal(valueSingleValue) } + +func TestSessionsEncodeDecode(t *testing.T) { + // test the sessions encode decode via gorilla.securecookie + values := map[string]interface{}{ + "Name": "iris", + "Months": "4", + "Secret": "dsads£2132215£%%Ssdsa", + } + app := iris.New() + app.Adapt(httprouter.New()) + // IMPORTANT + cookieName := "mycustomsessionid" + // AES only supports key sizes of 16, 24 or 32 bytes. + // You either need to provide exactly that amount or you derive the key from what you type in. + hashKey := []byte("the-big-and-secret-fash-key-here") + blockKey := []byte("lot-secret-of-characters-big-too") + secureCookie := securecookie.New(hashKey, blockKey) + + app.Adapt(sessions.New(sessions.Config{ + Cookie: cookieName, + Encode: secureCookie.Encode, + Decode: secureCookie.Decode, + })) + // + + writeValues := func(ctx *iris.Context) { + sessValues := ctx.Session().GetAll() + ctx.JSON(iris.StatusOK, sessValues) + } + + if testEnableSubdomain { + app.Party(testSubdomain+".").Get("/get", func(ctx *iris.Context) { + writeValues(ctx) + }) + } + + app.Post("set", func(ctx *iris.Context) { + vals := make(map[string]interface{}, 0) + if err := ctx.ReadJSON(&vals); err != nil { + t.Fatalf("Cannot readjson. Trace %s", err.Error()) + } + for k, v := range vals { + ctx.Session().Set(k, v) + } + }) + + app.Get("/get", func(ctx *iris.Context) { + writeValues(ctx) + }) + + app.Get("/clear", func(ctx *iris.Context) { + ctx.Session().Clear() + writeValues(ctx) + }) + + app.Get("/destroy", func(ctx *iris.Context) { + ctx.SessionDestroy() + writeValues(ctx) + // the cookie and all values should be empty + }) + + // request cookie should be empty + app.Get("/after_destroy", func(ctx *iris.Context) { + }) + app.Config.VHost = "mydomain.com" + e := httptest.New(app, t) + + e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() + e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values) + if testEnableSubdomain { + es := subdomainTester(e, app) + es.Request("GET", "/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values) + } + + // test destroy which also clears first + d := e.GET("/destroy").Expect().Status(iris.StatusOK) + d.JSON().Object().Empty() + // This removed: d.Cookies().Empty(). Reason: + // httpexpect counts the cookies setted or deleted at the response time, but cookie is not removed, to be really removed needs to SetExpire(now-1second) so, + // test if the cookies removed on the next request, like the browser's behavior. + e.GET("/after_destroy").Expect().Status(iris.StatusOK).Cookies().Empty() + // set and clear again + e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() + e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty() +}