mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
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
This commit is contained in:
parent
549c8eba10
commit
d51c0b7b50
|
@ -31,6 +31,29 @@ type (
|
||||||
// Defaults to false
|
// Defaults to false
|
||||||
DecodeCookie bool
|
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)).
|
// 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.
|
// If you want to delete the cookie when the browser closes, set it to -1.
|
||||||
//
|
//
|
||||||
|
|
|
@ -90,13 +90,16 @@ func (s *sessions) Start(res http.ResponseWriter, req *http.Request) iris.Sessio
|
||||||
var sess iris.Session
|
var sess iris.Session
|
||||||
|
|
||||||
cookieValue := GetCookie(s.config.Cookie, req)
|
cookieValue := GetCookie(s.config.Cookie, req)
|
||||||
|
|
||||||
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
|
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
|
||||||
sid := SessionIDGenerator(s.config.CookieLength)
|
sid := SessionIDGenerator(s.config.CookieLength)
|
||||||
|
|
||||||
sess = s.provider.Init(sid, s.config.Expires)
|
sess = s.provider.Init(sid, s.config.Expires)
|
||||||
cookie := &http.Cookie{}
|
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
|
// 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.Name = s.config.Cookie
|
||||||
|
|
||||||
cookie.Value = sid
|
cookie.Value = sid
|
||||||
cookie.Path = "/"
|
cookie.Path = "/"
|
||||||
if !s.config.DisableSubdomainPersistence {
|
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())
|
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)
|
AddCookie(cookie, res)
|
||||||
} else {
|
} 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)
|
sess = s.provider.Read(cookieValue, s.config.Expires)
|
||||||
}
|
}
|
||||||
return sess
|
return sess
|
||||||
|
@ -157,6 +186,22 @@ func (s *sessions) Destroy(res http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
RemoveCookie(s.config.Cookie, res, req)
|
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)
|
s.provider.Destroy(cookieValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
28
context.go
28
context.go
|
@ -269,6 +269,34 @@ func (ctx *Context) GetHandlerName() string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name()
|
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.
|
// 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
|
// 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
|
// BUT it isn't available by browsing, its handlers executed only when other handler's context call them
|
||||||
|
|
43
iris.go
43
iris.go
|
@ -814,6 +814,7 @@ func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc {
|
||||||
ctx.EmitError(StatusInternalServerError)
|
ctx.EmitError(StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// just to check if router is adapted.
|
||||||
wp := s.policies.RouterReversionPolicy.WildcardPath
|
wp := s.policies.RouterReversionPolicy.WildcardPath
|
||||||
if wp == nil {
|
if wp == nil {
|
||||||
s.Log(ProdMode, "regex cannot be used when a router policy is missing\n"+errRouterIsMissing.Format(s.Config.VHost).Error())
|
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")
|
"paramName2, expression2. The len should be %2==0")
|
||||||
return srvErr
|
return srvErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we do compile first to reduce the performance cost at serve time.
|
||||||
pairs := make(map[string]*regexp.Regexp, len(pairParamExpr)/2)
|
pairs := make(map[string]*regexp.Regexp, len(pairParamExpr)/2)
|
||||||
|
|
||||||
for i := 0; i < len(pairParamExpr)-1; i++ {
|
for i := 0; i < len(pairParamExpr)-1; i++ {
|
||||||
|
@ -843,18 +846,7 @@ func (s *Framework) Regex(pairParamExpr ...string) HandlerFunc {
|
||||||
// return the middleware
|
// return the middleware
|
||||||
return func(ctx *Context) {
|
return func(ctx *Context) {
|
||||||
for k, v := range pairs {
|
for k, v := range pairs {
|
||||||
pathPart := ctx.Param(k)
|
if !ctx.ParamValidate(v, 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) {
|
|
||||||
ctx.EmitError(StatusNotFound)
|
ctx.EmitError(StatusNotFound)
|
||||||
return
|
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.
|
// RouteParam returns a named parameter as each router defines named path parameters.
|
||||||
// For example, with the httprouter(: as named param symbol):
|
// For example, with the httprouter(: as named param symbol):
|
||||||
// userid should return :userid.
|
// userid should return :userid.
|
||||||
|
|
|
@ -3,6 +3,7 @@ package iris_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie" // optional, to set sessions'' Encode and Decode
|
||||||
"gopkg.in/kataras/iris.v6"
|
"gopkg.in/kataras/iris.v6"
|
||||||
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
|
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
|
||||||
"gopkg.in/kataras/iris.v6/adaptors/sessions"
|
"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/").Expect().Status(http.StatusOK).JSON().Object().Equal(values)
|
||||||
e.GET("/get_single").Expect().Status(iris.StatusOK).Body().Equal(valueSingleValue)
|
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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user