enhanced cookie security and management

Former-commit-id: a97b0b33e87749a2e8c32e63269fcc60fa326ff3
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-05-09 14:04:51 +03:00
parent d5f1649895
commit 50b18c7515
18 changed files with 490 additions and 466 deletions

View File

@ -371,12 +371,13 @@ Other Improvements:
![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0) ![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0)
- Enhanced cookie security and management through new `Context.AddCookieOptions` method and new cookie options (look on New Package-level functions section below), [securecookie](https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie) example has been updated.
- `iris.TLS` can now accept certificates as raw contents too. - `iris.TLS` can now accept certificates as raw contents too.
- `iris.TLS` registers a secondary http server which redirects "http://" to their "https://" equivalent requests, unless the new `iris.TLSNoRedirect` host Configurator is provided on `iris.TLS` (or `iris.AutoTLS`), e.g. `app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key", iris.TLSNoRedirect))`. - `iris.TLS` registers a secondary http server which redirects "http://" to their "https://" equivalent requests, unless the new `iris.TLSNoRedirect` host Configurator is provided on `iris.TLS` (or `iris.AutoTLS`), e.g. `app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key", iris.TLSNoRedirect))`.
- Fix an [issue](https://github.com/kataras/i18n/issues/1) about i18n loading from path which contains potential language code. - Fix an [issue](https://github.com/kataras/i18n/issues/1) about i18n loading from path which contains potential language code.
- Server will not return neither log the `ErrServerClosed` if `app.Shutdown` was called manually via interrupt signal(CTRL/CMD+C), note that if the server closed by any other reason the error will be fired as previously (unless `iris.WithoutServerError(iris.ErrServerClosed)`). - Server will not return neither log the `ErrServerClosed` error if `app.Shutdown` was called manually via interrupt signal(CTRL/CMD+C), note that if the server closed by any other reason the error will be fired as previously (unless `iris.WithoutServerError(iris.ErrServerClosed)`).
- Finally, Log level's and Route debug information colorization is respected across outputs. Previously if the application used more than one output destination (e.g. a file through `app.Logger().AddOutput`) the color support was automatically disabled from all, including the terminal one, this problem is fixed now. Developers can now see colors in their terminals while log files are kept with clear text. - Finally, Log level's and Route debug information colorization is respected across outputs. Previously if the application used more than one output destination (e.g. a file through `app.Logger().AddOutput`) the color support was automatically disabled from all, including the terminal one, this problem is fixed now. Developers can now see colors in their terminals while log files are kept with clear text.
@ -396,8 +397,16 @@ Other Improvements:
- Fix [#1473](https://github.com/kataras/iris/issues/1473). - Fix [#1473](https://github.com/kataras/iris/issues/1473).
New Package-level Variables:
- `iris.B, KB, MB, GB, TB, PB, EB` for byte units.
- `TLSNoRedirect` to disable automatic "http://" to "https://" redirections (see below)
- `CookieAllowReclaim`, `CookieAllowSubdomains`, `CookieSameSite`, `CookieSecure` and `CookieEncoding` to bring previously sessions-only features to all cookies in the request.
New Context Methods: New Context Methods:
- `Context.AddCookieOptions(...CookieOption)` adds options for `SetCookie`, `SetCookieKV, UpsertCookie` and `RemoveCookie` methods for the current request.
- `Context.ClearCookieOptions()` clears any cookie options registered through `AddCookieOptions`.
- `Context.SetVersion(constraint string)` force-sets an [API Version](https://github.com/kataras/iris/wiki/API-versioning) - `Context.SetVersion(constraint string)` force-sets an [API Version](https://github.com/kataras/iris/wiki/API-versioning)
- `Context.SetLanguage(langCode string)` force-sets a language code from inside a middleare, similar to the `app.I18n.ExtractFunc` - `Context.SetLanguage(langCode string)` force-sets a language code from inside a middleare, similar to the `app.I18n.ExtractFunc`
- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client - `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client
@ -421,6 +430,8 @@ New Context Methods:
Breaking Changes: Breaking Changes:
- `iris.CookieEncode` and `CookieDecode` are replaced with the `iris.CookieEncoding`.
- `sessions#Config.Encode` and `Decode` are removed in favor of (the existing) `Encoding` field.
- `versioning.GetVersion` now returns an empty string if version wasn't found. - `versioning.GetVersion` now returns an empty string if version wasn't found.
- Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See <https://go-review.googlesource.com/c/go/+/186927/>. - Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See <https://go-review.googlesource.com/c/go/+/186927/>.
- Remove the last input argument of `enableGzipCompression` in `Context.ServeContent`, `ServeFile` methods. This was deprecated a few versions ago. A middleware (`app.Use(iris.Gzip)`) or a prior call to `Context.Gzip(true)` will enable gzip compression. Also these two methods and `Context.SendFile` one now support `Content-Range` and `Accept-Ranges` correctly out of the box (`net/http` had a bug, which is now fixed). - Remove the last input argument of `enableGzipCompression` in `Context.ServeContent`, `ServeFile` methods. This was deprecated a few versions ago. A middleware (`app.Use(iris.Gzip)`) or a prior call to `Context.Gzip(true)` will enable gzip compression. Also these two methods and `Context.SendFile` one now support `Content-Range` and `Accept-Ranges` correctly out of the box (`net/http` had a bug, which is now fixed).

View File

@ -74,16 +74,14 @@ var sessionsManager *sessions.Sessions
func init() { func init() {
// attach a session manager // attach a session manager
cookieName := "mycustomsessionid" cookieName := "mycustomsessionid"
// AES only supports key sizes of 16, 24 or 32 bytes. hashKey := securecookie.GenerateRandomKey(64)
// You either need to provide exactly that amount or you derive the key from what you type in. blockKey := securecookie.GenerateRandomKey(32)
hashKey := []byte("the-big-and-secret-fash-key-here")
blockKey := []byte("lot-secret-of-characters-big-too")
secureCookie := securecookie.New(hashKey, blockKey) secureCookie := securecookie.New(hashKey, blockKey)
sessionsManager = sessions.New(sessions.Config{ sessionsManager = sessions.New(sessions.Config{
Cookie: cookieName, Cookie: cookieName,
Encode: secureCookie.Encode, Encoding: secureCookie,
Decode: secureCookie.Decode, AllowReclaim: true,
}) })
} }

View File

@ -13,16 +13,16 @@ func TestCookiesBasic(t *testing.T) {
cookieName, cookieValue := "my_cookie_name", "my_cookie_value" cookieName, cookieValue := "my_cookie_name", "my_cookie_value"
// Test Set A Cookie. // Test set a Cookie.
t1 := e.GET(fmt.Sprintf("/cookies/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK) t1 := e.GET(fmt.Sprintf("/cookies/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK)
t1.Cookie(cookieName).Value().Equal(cookieValue) // validate cookie's existence, it should be there now. t1.Cookie(cookieName).Value().Equal(cookieValue) // validate cookie's existence, it should be there now.
t1.Body().Contains(cookieValue) t1.Body().Contains(cookieValue)
// Test Retrieve A Cookie. // Test retrieve a Cookie.
t2 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t2 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK)
t2.Body().Equal(cookieValue) t2.Body().Equal(cookieValue)
// Test Remove A Cookie. // Test remove a Cookie.
t3 := e.DELETE(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t3 := e.DELETE(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK)
t3.Body().Contains(cookieName) t3.Body().Contains(cookieName)

View File

@ -11,49 +11,62 @@ import (
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
) )
var ( func main() {
// AES only supports key sizes of 16, 24 or 32 bytes. app := newApp()
// You either need to provide exactly that amount or you derive the key from what you type in. // http://localhost:8080/cookies/name/value
hashKey = []byte("the-big-and-secret-fash-key-here") // http://localhost:8080/cookies/name
blockKey = []byte("lot-secret-of-characters-big-too") // http://localhost:8080/cookies/remove/name
sc = securecookie.New(hashKey, blockKey) app.Listen(":8080")
) }
func newApp() *iris.Application { func newApp() *iris.Application {
app := iris.New() app := iris.New()
r := app.Party("/cookies")
{
r.Use(useSecureCookies())
// Set A Cookie. // Set A Cookie.
app.Get("/cookies/{name}/{value}", func(ctx iris.Context) { r.Get("/{name}/{value}", func(ctx iris.Context) {
name := ctx.Params().Get("name") name := ctx.Params().Get("name")
value := ctx.Params().Get("value") value := ctx.Params().Get("value")
ctx.SetCookieKV(name, value, iris.CookieEncode(sc.Encode)) // <-- ctx.SetCookieKV(name, value)
ctx.Writef("cookie added: %s = %s", name, value) ctx.Writef("cookie added: %s = %s", name, value)
}) })
// Retrieve A Cookie. // Retrieve A Cookie.
app.Get("/cookies/{name}", func(ctx iris.Context) { r.Get("/{name}", func(ctx iris.Context) {
name := ctx.Params().Get("name") name := ctx.Params().Get("name")
value := ctx.GetCookie(name, iris.CookieDecode(sc.Decode)) // <-- value := ctx.GetCookie(name)
ctx.WriteString(value) ctx.WriteString(value)
}) })
// Delete A Cookie. r.Get("/remove/{name}", func(ctx iris.Context) {
app.Delete("/cookies/{name}", func(ctx iris.Context) {
name := ctx.Params().Get("name") name := ctx.Params().Get("name")
ctx.RemoveCookie(name) // <-- ctx.RemoveCookie(name)
ctx.Writef("cookie %s removed", name) ctx.Writef("cookie %s removed", name)
}) })
}
return app return app
} }
func main() { func useSecureCookies() iris.Handler {
app := newApp() var (
app.Listen(":8080") hashKey = securecookie.GenerateRandomKey(64)
blockKey = securecookie.GenerateRandomKey(32)
s = securecookie.New(hashKey, blockKey)
)
return func(ctx iris.Context) {
ctx.AddCookieOptions(iris.CookieEncoding(s))
ctx.Next()
}
} }

View File

@ -7,25 +7,25 @@ import (
"github.com/kataras/iris/v12/httptest" "github.com/kataras/iris/v12/httptest"
) )
func TestCookiesBasic(t *testing.T) { func TestSecureCookie(t *testing.T) {
app := newApp() app := newApp()
e := httptest.New(t, app, httptest.URL("http://example.com")) e := httptest.New(t, app, httptest.URL("http://example.com"))
cookieName, cookieValue := "my_cookie_name", "my_cookie_value" cookieName, cookieValue := "my_cookie_name", "my_cookie_value"
// Test Set A Cookie. // Test set a Cookie.
t1 := e.GET(fmt.Sprintf("/cookies/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK) t1 := e.GET(fmt.Sprintf("/cookies/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK)
// note that this will not work because it doesn't always returns the same value: // note that this will not work because it doesn't always returns the same value:
// cookieValueEncoded, _ := sc.Encode(cookieName, cookieValue) // cookieValueEncoded, _ := sc.Encode(cookieName, cookieValue)
t1.Cookie(cookieName).Value().NotEqual(cookieValue) // validate cookie's existence and value is not on its raw form. t1.Cookie(cookieName).Value().NotEqual(cookieValue) // validate cookie's existence and value is not on its raw form.
t1.Body().Contains(cookieValue) t1.Body().Contains(cookieValue)
// Test Retrieve A Cookie. // Test retrieve a Cookie.
t2 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t2 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK)
t2.Body().Equal(cookieValue) t2.Body().Equal(cookieValue)
// Test Remove A Cookie. // Test remove a Cookie.
t3 := e.DELETE(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t3 := e.GET(fmt.Sprintf("/cookies/remove/%s", cookieName)).Expect().Status(httptest.StatusOK)
t3.Body().Contains(cookieName) t3.Body().Contains(cookieName)
t4 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK) t4 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK)

View File

@ -1 +0,0 @@
![tunneling_screenshot.png](tunneling_screenshot.png)

View File

@ -14,7 +14,7 @@ func main() {
ctx.Application().ConfigurationReadOnly().GetVHost()) ctx.Application().ConfigurationReadOnly().GetVHost())
}) })
app.Listen(":8080", iris.WithTunneling) app.Listen(":8080", iris.WithTunneling, iris.WithLogLevel("debug"))
/* The full configuration can be set as: /* The full configuration can be set as:
app.Listen(":8080", iris.WithConfiguration( app.Listen(":8080", iris.WithConfiguration(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

View File

@ -10,7 +10,7 @@ import (
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
// $ go get github.com/go-playground/validator/v10 // $ go get github.com/go-playground/validator/v10@latest
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
) )

View File

@ -20,14 +20,13 @@ func newApp() *iris.Application {
cookieName := "_session_id" cookieName := "_session_id"
// AES only supports key sizes of 16, 24 or 32 bytes. // 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. // 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") hashKey := securecookie.GenerateRandomKey(64)
blockKey := []byte("lot-secret-of-characters-big-too") blockKey := securecookie.GenerateRandomKey(32)
secureCookie := securecookie.New(hashKey, blockKey) s := securecookie.New(hashKey, blockKey)
mySessions := sessions.New(sessions.Config{ mySessions := sessions.New(sessions.Config{
Cookie: cookieName, Cookie: cookieName,
Encode: secureCookie.Encode, Encoding: s,
Decode: secureCookie.Decode,
AllowReclaim: true, AllowReclaim: true,
}) })

View File

@ -879,7 +879,7 @@ type Configuration struct {
LanguageContextKey string `json:"languageContextKey,omitempty" yaml:"LanguageContextKey" toml:"LanguageContextKey"` LanguageContextKey string `json:"languageContextKey,omitempty" yaml:"LanguageContextKey" toml:"LanguageContextKey"`
// VersionContextKey is the context key which an API Version can be modified // VersionContextKey is the context key which an API Version can be modified
// via a middleware through `SetVersion` method, e.g. `ctx.SetVersion("1.0, 1.1")`. // via a middleware through `SetVersion` method, e.g. `ctx.SetVersion("1.0, 1.1")`.
// Defauls to "iris.api.version". // Defaults to "iris.api.version".
VersionContextKey string `json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"` VersionContextKey string `json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"`
// GetViewLayoutContextKey is the key of the context's user values' key // GetViewLayoutContextKey is the key of the context's user values' key
// which is being used to set the template // which is being used to set the template

View File

@ -36,6 +36,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5"
"golang.org/x/net/publicsuffix"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -984,6 +985,25 @@ type Context interface {
// | Cookies | // | Cookies |
// +------------------------------------------------------------+ // +------------------------------------------------------------+
// AddCookieOptions adds cookie options for `SetCookie`,
// `SetCookieKV, UpsertCookie` and `RemoveCookie` methods
// for the current request. It can be called from a middleware before
// cookies sent or received from the next Handler in the chain.
// See `ClearCookieOptions` too.
//
// Available builtin Cookie options are:
// * CookieSameSite
// * CookiePath
// * CookieCleanPath
// * CookieExpires
// * CookieHTTPOnly
// * CookieEncoding
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
AddCookieOptions(options ...CookieOption)
// ClearCookieOptions clears any previously registered cookie options.
// See `AddCookieOptions` too.
ClearCookieOptions()
// SetCookie adds a cookie. // SetCookie adds a cookie.
// Use of the "options" is not required, they can be used to amend the "cookie". // Use of the "options" is not required, they can be used to amend the "cookie".
// //
@ -994,14 +1014,6 @@ type Context interface {
// if already set by a previous `SetCookie` call. // if already set by a previous `SetCookie` call.
// It reports whether the cookie is new (true) or an existing one was updated (false). // It reports whether the cookie is new (true) or an existing one was updated (false).
UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool
// SetSameSite sets a same-site rule for cookies to set.
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests. The main
// goal is to mitigate the risk of cross-origin information leakage, and provide
// some protection against cross-site request forgery attacks.
//
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
SetSameSite(sameSite http.SameSite)
// SetCookieKV adds a cookie, requires the name(string) and the value(string). // SetCookieKV adds a cookie, requires the name(string) and the value(string).
// //
// By default it expires at 2 hours and it's added to the root path, // By default it expires at 2 hours and it's added to the root path,
@ -4734,48 +4746,171 @@ func (ctx *context) SendFileWithRate(src, destName string, limit float64, burst
// | Cookies | // | Cookies |
// +------------------------------------------------------------+ // +------------------------------------------------------------+
// Set of Cookie actions for `CookieOption`.
const (
OpCookieGet uint8 = iota
OpCookieSet
OpCookieDel
)
// CookieOption is the type of function that is accepted on // CookieOption is the type of function that is accepted on
// context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie` // context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie`
// as their (last) variadic input argument to amend the end cookie's form. // as their (last) variadic input argument to amend the end cookie's form.
// //
// Any custom or builtin `CookieOption` is valid, // Any custom or builtin `CookieOption` is valid,
// see `CookiePath`, `CookieCleanPath`, `CookieExpires` and `CookieHTTPOnly` for more. // see `CookiePath`, `CookieCleanPath`, `CookieExpires` and `CookieHTTPOnly` for more.
type CookieOption func(*http.Cookie) // The "op" is the operation code, 0 is GET, 1 is SET and 2 is REMOVE.
type CookieOption func(c *http.Cookie, op uint8)
// CookiePath is a `CookieOption`. // findCookieAgainst reports whether the "cookie.Name" is in the list of "cookieNames".
// Use it to change the cookie's Path field. // Notes:
func CookiePath(path string) CookieOption { // If "cookieNames" slice is empty then it returns true,
return func(c *http.Cookie) { // If "cookie.Name" is empty then it returns false.
c.Path = path func findCookieAgainst(cookie *http.Cookie, cookieNames []string) bool {
if cookie.Name == "" {
return false
}
if len(cookieNames) > 0 {
for _, name := range cookieNames {
if cookie.Name == name {
return true
} }
} }
// CookieCleanPath is a `CookieOption`. return false
// Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`.
func CookieCleanPath(c *http.Cookie) {
c.Path = ""
} }
// CookieExpires is a `CookieOption`. return true
// Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie. }
func CookieExpires(durFromNow time.Duration) CookieOption {
return func(c *http.Cookie) { // CookieAllowReclaim accepts the Context itself.
c.Expires = time.Now().Add(durFromNow) // If set it will add the cookie to (on `CookieSet`, `CookieSetKV`, `CookieUpsert`)
c.MaxAge = int(durFromNow.Seconds()) // or remove the cookie from (on `CookieRemove`) the Request object too.
func CookieAllowReclaim(ctx Context, cookieNames ...string) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieGet {
return
}
if !findCookieAgainst(c, cookieNames) {
return
}
switch op {
case OpCookieSet:
ctx.Request().AddCookie(c)
case OpCookieDel:
// TODO: delete only this c.Name.
ctx.Request().Header.Set("Cookie", "")
}
}
}
// CookieAllowSubdomains set to the Cookie Options
// in order to allow subdomains to have access to the cookies.
// It sets the cookie's Domain field (if was empty) and
// it also sets the cookie's SameSite to lax mode too.
func CookieAllowSubdomains(ctx Context, cookieNames ...string) CookieOption {
host := ctx.Host()
if portIdx := strings.IndexByte(host, ':'); portIdx > 0 {
host = host[0:portIdx]
}
cookieDomain := "." + host
if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
cookieDomain = "." + domain
}
return func(c *http.Cookie, _ uint8) {
if c.Domain != "" {
return // already set.
}
if !findCookieAgainst(c, cookieNames) {
return
}
c.Domain = cookieDomain
c.SameSite = http.SameSiteLaxMode // allow subdomain sharing.
}
}
// CookieSameSite sets a same-site rule for cookies to set.
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests. The main
// goal is to mitigate the risk of cross-origin information leakage, and provide
// some protection against cross-site request forgery attacks.
//
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
func CookieSameSite(sameSite http.SameSite) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
c.SameSite = sameSite
}
}
}
// CookieSecure sets the cookie's Secure option if the current request's
// connection is using TLS. See `CookieHTTPOnly` too.
func CookieSecure(ctx Context) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
if ctx.Request().TLS != nil {
c.Secure = true
}
}
} }
} }
// CookieHTTPOnly is a `CookieOption`. // CookieHTTPOnly is a `CookieOption`.
// Use it to set the cookie's HttpOnly field to false or true. // Use it to set the cookie's HttpOnly field to false or true.
// HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`. // HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`.
// See `CookieSecure` too.
func CookieHTTPOnly(httpOnly bool) CookieOption { func CookieHTTPOnly(httpOnly bool) CookieOption {
return func(c *http.Cookie) { return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
c.HttpOnly = httpOnly c.HttpOnly = httpOnly
} }
} }
}
type ( // CookiePath is a `CookieOption`.
// CookieEncoder should encode the cookie value. // Use it to change the cookie's Path field.
func CookiePath(path string) CookieOption {
return func(c *http.Cookie, op uint8) {
if op > OpCookieGet { // on set and remove.
c.Path = path
}
}
}
// CookieCleanPath is a `CookieOption`.
// Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`.
func CookieCleanPath(c *http.Cookie, op uint8) {
if op > OpCookieGet {
c.Path = ""
}
}
// CookieExpires is a `CookieOption`.
// Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie.
func CookieExpires(durFromNow time.Duration) CookieOption {
return func(c *http.Cookie, op uint8) {
if op == OpCookieSet {
c.Expires = time.Now().Add(durFromNow)
c.MaxAge = int(durFromNow.Seconds())
}
}
}
// SecureCookie should encodes and decodes
// authenticated and optionally encrypted cookie values.
// See `CookieEncoding` package-level function.
type SecureCookie interface {
// Encode should encode the cookie value.
// Should accept the cookie's name as its first argument // Should accept the cookie's name as its first argument
// and as second argument the cookie value ptr. // and as second argument the cookie value ptr.
// Should return an encoded value or an empty one if encode operation failed. // Should return an encoded value or an empty one if encode operation failed.
@ -4785,9 +4920,9 @@ type (
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. // 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. // You either need to provide exactly that amount or you derive the key from what you type in.
// //
// See `CookieDecoder` too. // See `Decode` too.
CookieEncoder func(cookieName string, value interface{}) (string, error) Encode(cookieName string, cookieValue interface{}) (string, error)
// CookieDecoder should decode the cookie value. // Decode should decode the cookie value.
// Should accept the cookie's name as its first argument, // Should accept the cookie's name as its first argument,
// as second argument the encoded cookie value and as third argument the decoded value ptr. // as second argument the encoded cookie value and as third argument the decoded value ptr.
// Should return a decoded value or an empty one if decode operation failed. // Should return a decoded value or an empty one if decode operation failed.
@ -4797,69 +4932,123 @@ type (
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. // 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. // You either need to provide exactly that amount or you derive the key from what you type in.
// //
// See `CookieEncoder` too. // See `Encode` too.
CookieDecoder func(cookieName string, cookieValue string, v interface{}) error Decode(cookieName string, cookieValue string, cookieValuePtr interface{}) error
) }
// CookieEncode is a `CookieOption`. // CookieEncoding accepts a value which implements `Encode` and `Decode` methods.
// Provides encoding functionality when adding a cookie. // It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods.
// Accepts a `CookieEncoder` and sets the cookie's value to the encoded value. // And on `Context.GetCookie` method it calls its `Decode`.
// Users of that is the `SetCookie` and `SetCookieKV`. // If "cookieNames" slice is not empty then only cookies
// with that `Name` will be encoded on set and decoded on get, that way you can encrypt
// specific cookie names (like the session id) and let the rest of the cookies "insecure".
// //
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
func CookieEncode(encode CookieEncoder) CookieOption { func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption {
return func(c *http.Cookie) { return func(c *http.Cookie, op uint8) {
newVal, err := encode(c.Name, c.Value) if op == OpCookieDel {
return
}
if !findCookieAgainst(c, cookieNames) {
return
}
switch op {
case OpCookieSet:
// Should encode, it's a write to the client operation.
newVal, err := encoding.Encode(c.Name, c.Value)
if err != nil { if err != nil {
c.Value = "" c.Value = ""
} else { } else {
c.Value = newVal c.Value = newVal
} }
} return
} case OpCookieGet:
// Should decode, it's a read from the client operation.
// CookieDecode is a `CookieOption`. if err := encoding.Decode(c.Name, c.Value, &c.Value); err != nil {
// Provides decoding functionality when retrieving a cookie.
// Accepts a `CookieDecoder` and sets the cookie's value to the decoded value before return by the `GetCookie`.
// User of that is the `GetCookie`.
//
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
func CookieDecode(decode CookieDecoder) CookieOption {
return func(c *http.Cookie) {
if err := decode(c.Name, c.Value, &c.Value); err != nil {
c.Value = "" c.Value = ""
} }
} }
} }
}
const cookieOptionsContextKey = "iris.cookie.options"
// AddCookieOptions adds cookie options for `SetCookie`,
// `SetCookieKV, UpsertCookie` and `RemoveCookie` methods
// for the current request. It can be called from a middleware before
// cookies sent or received from the next Handler in the chain.
//
// Available builtin Cookie options are:
// * CookieAllowReclaim
// * CookieAllowSubdomains
// * CookieSecure
// * CookieHTTPOnly
// * CookieSameSite
// * CookiePath
// * CookieCleanPath
// * CookieExpires
// * CookieEncoding
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
func (ctx *context) AddCookieOptions(options ...CookieOption) {
if len(options) == 0 {
return
}
if v := ctx.Values().Get(cookieOptionsContextKey); v != nil {
if opts, ok := v.([]CookieOption); ok {
options = append(opts, options...)
}
}
ctx.Values().Set(cookieOptionsContextKey, options)
}
func (ctx *context) applyCookieOptions(c *http.Cookie, op uint8, override []CookieOption) {
if v := ctx.Values().Get(cookieOptionsContextKey); v != nil {
if options, ok := v.([]CookieOption); ok {
for _, opt := range options {
opt(c, op)
}
}
}
// The function's ones should be called last, so they can override
// the stored ones (i.e by a prior middleware).
for _, opt := range override {
opt(c, op)
}
}
// ClearCookieOptions clears any previously registered cookie options.
// See `AddCookieOptions` too.
func (ctx *context) ClearCookieOptions() {
ctx.Values().Remove(cookieOptionsContextKey)
}
// SetCookie adds a cookie. // SetCookie adds a cookie.
// Use of the "options" is not required, they can be used to amend the "cookie". // Use of the "options" is not required, they can be used to amend the "cookie".
// //
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic
func (ctx *context) SetCookie(cookie *http.Cookie, options ...CookieOption) { func (ctx *context) SetCookie(cookie *http.Cookie, options ...CookieOption) {
cookie.SameSite = GetSameSite(ctx) ctx.applyCookieOptions(cookie, OpCookieSet, options)
for _, opt := range options {
opt(cookie)
}
http.SetCookie(ctx.writer, cookie) http.SetCookie(ctx.writer, cookie)
} }
const setCookieHeaderKey = "Set-Cookie"
// UpsertCookie adds a cookie to the response like `SetCookie` does // UpsertCookie adds a cookie to the response like `SetCookie` does
// but it will also perform a replacement of the cookie // but it will also perform a replacement of the cookie
// if already set by a previous `SetCookie` call. // if already set by a previous `SetCookie` call.
// It reports whether the cookie is new (true) or an existing one was updated (false). // It reports whether the cookie is new (true) or an existing one was updated (false).
func (ctx *context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool { func (ctx *context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool {
cookie.SameSite = GetSameSite(ctx) ctx.applyCookieOptions(cookie, OpCookieSet, options)
for _, opt := range options {
opt(cookie)
}
header := ctx.ResponseWriter().Header() header := ctx.ResponseWriter().Header()
if cookies := header["Set-Cookie"]; len(cookies) > 0 { if cookies := header[setCookieHeaderKey]; len(cookies) > 0 {
s := cookie.Name + "=" // name=?value s := cookie.Name + "=" // name=?value
for i, c := range cookies { for i, c := range cookies {
if strings.HasPrefix(c, s) { if strings.HasPrefix(c, s) {
@ -4867,40 +5056,21 @@ func (ctx *context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) b
// Probably the cookie is set and then updated in the first session creation // Probably the cookie is set and then updated in the first session creation
// (e.g. UpdateExpiration, see https://github.com/kataras/iris/issues/1485). // (e.g. UpdateExpiration, see https://github.com/kataras/iris/issues/1485).
cookies[i] = cookie.String() cookies[i] = cookie.String()
header["Set-Cookie"] = cookies header[setCookieHeaderKey] = cookies
return false return false
} }
} }
} }
header.Add("Set-Cookie", cookie.String()) header.Add(setCookieHeaderKey, cookie.String())
return true return true
} }
const sameSiteContextKey = "iris.cookie_same_site" // SetCookieKVExpiration is 365 days by-default
// you can change it or simple, use the SetCookie for more control.
// SetSameSite sets a same-site rule for cookies to set.
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests. The main
// goal is to mitigate the risk of cross-origin information leakage, and provide
// some protection against cross-site request forgery attacks.
// //
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. // See CookieExpires` for more.
func (ctx *context) SetSameSite(sameSite http.SameSite) { var SetCookieKVExpiration = time.Duration(8760) * time.Hour
ctx.Values().Set(sameSiteContextKey, sameSite)
}
// GetSameSite returns the saved-to-context cookie http.SameSite option.
func GetSameSite(ctx Context) http.SameSite {
if v := ctx.Values().Get(sameSiteContextKey); v != nil {
sameSite, ok := v.(http.SameSite)
if ok {
return sameSite
}
}
return http.SameSiteDefaultMode
}
// SetCookieKV adds a cookie, requires the name(string) and the value(string). // SetCookieKV adds a cookie, requires the name(string) and the value(string).
// //
@ -4925,8 +5095,13 @@ func (ctx *context) SetCookieKV(name, value string, options ...CookieOption) {
c.Name = name c.Name = name
c.Value = url.QueryEscape(value) c.Value = url.QueryEscape(value)
c.HttpOnly = true c.HttpOnly = true
// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
c.Expires = time.Now().Add(SetCookieKVExpiration) c.Expires = time.Now().Add(SetCookieKVExpiration)
c.MaxAge = int(SetCookieKVExpiration.Seconds()) c.MaxAge = int(time.Until(c.Expires).Seconds())
ctx.SetCookie(c, options...) ctx.SetCookie(c, options...)
} }
@ -4938,24 +5113,24 @@ func (ctx *context) SetCookieKV(name, value string, options ...CookieOption) {
// //
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic // Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic
func (ctx *context) GetCookie(name string, options ...CookieOption) string { func (ctx *context) GetCookie(name string, options ...CookieOption) string {
cookie, err := ctx.request.Cookie(name) c, err := ctx.request.Cookie(name)
if err != nil { if err != nil {
return "" return ""
} }
for _, opt := range options { ctx.applyCookieOptions(c, OpCookieGet, options)
opt(cookie)
}
value, _ := url.QueryUnescape(cookie.Value) value, _ := url.QueryUnescape(c.Value)
return value return value
} }
// SetCookieKVExpiration is 365 days by-default var (
// you can change it or simple, use the SetCookie for more control. // CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
// CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
// See `SetCookieKVExpiration` and `CookieExpires` for more.
var SetCookieKVExpiration = time.Duration(8760) * time.Hour // CookieExpireUnlimited indicates that does expires after 24 years.
CookieExpireUnlimited = time.Now().AddDate(24, 10, 10)
)
// RemoveCookie deletes a cookie by its name and path = "/". // RemoveCookie deletes a cookie by its name and path = "/".
// Tip: change the cookie's path to the current one by: RemoveCookie("name", iris.CookieCleanPath) // Tip: change the cookie's path to the current one by: RemoveCookie("name", iris.CookieCleanPath)
@ -4967,13 +5142,13 @@ func (ctx *context) RemoveCookie(name string, options ...CookieOption) {
c.Value = "" c.Value = ""
c.Path = "/" // if user wants to change it, use of the CookieOption `CookiePath` is required if not `ctx.SetCookie`. c.Path = "/" // if user wants to change it, use of the CookieOption `CookiePath` is required if not `ctx.SetCookie`.
c.HttpOnly = true c.HttpOnly = true
// RFC says 1 second, but let's do it 1 to make sure is working // RFC says 1 second, but let's do it 1 to make sure is working
exp := time.Now().Add(-time.Duration(1) * time.Minute) c.Expires = CookieExpireDelete
c.Expires = exp
c.MaxAge = -1 c.MaxAge = -1
ctx.SetCookie(c, options...)
// delete request's cookie also, which is temporary available. ctx.applyCookieOptions(c, OpCookieDel, options)
ctx.request.Header.Set("Cookie", "") http.SetCookie(ctx.writer, c)
} }
// VisitAllCookies takes a visitor function which is called // VisitAllCookies takes a visitor function which is called

64
iris.go
View File

@ -493,6 +493,41 @@ var (
// //
// A shortcut of the `cache#Cache304`. // A shortcut of the `cache#Cache304`.
Cache304 = cache.Cache304 Cache304 = cache.Cache304
// CookieAllowReclaim accepts the Context itself.
// If set it will add the cookie to (on `CookieSet`, `CookieSetKV`, `CookieUpsert`)
// or remove the cookie from (on `CookieRemove`) the Request object too.
//
// A shortcut for the `context#CookieAllowReclaim`.
CookieAllowReclaim = context.CookieAllowReclaim
// CookieAllowSubdomains set to the Cookie Options
// in order to allow subdomains to have access to the cookies.
// It sets the cookie's Domain field (if was empty) and
// it also sets the cookie's SameSite to lax mode too.
//
// A shortcut for the `context#CookieAllowSubdomains`.
CookieAllowSubdomains = context.CookieAllowSubdomains
// CookieSameSite sets a same-site rule for cookies to set.
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests. The main
// goal is to mitigate the risk of cross-origin information leakage, and provide
// some protection against cross-site request forgery attacks.
//
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
//
// A shortcut for the `context#CookieSameSite`.
CookieSameSite = context.CookieHTTPOnly
// CookieSecure sets the cookie's Secure option if the current request's
// connection is using TLS. See `CookieHTTPOnly` too.
//
// A shortcut for the `context#CookieSecure`.
CookieSecure = context.CookieSecure
// CookieHTTPOnly is a `CookieOption`.
// Use it to set the cookie's HttpOnly field to false or true.
// HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`.
//
// A shortcut for the `context#CookieHTTPOnly`.
CookieHTTPOnly = context.CookieHTTPOnly
// CookiePath is a `CookieOption`. // CookiePath is a `CookieOption`.
// Use it to change the cookie's Path field. // Use it to change the cookie's Path field.
// //
@ -508,30 +543,13 @@ var (
// //
// A shortcut for the `context#CookieExpires`. // A shortcut for the `context#CookieExpires`.
CookieExpires = context.CookieExpires CookieExpires = context.CookieExpires
// CookieHTTPOnly is a `CookieOption`. // CookieEncoding accepts a value which implements `Encode` and `Decode` methods.
// Use it to set the cookie's HttpOnly field to false or true. // It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods.
// HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`. // And on `Context.GetCookie` method it calls its `Decode`.
// //
// A shortcut for the `context#CookieHTTPOnly`. // A shortcut for the `context#CookieEncoding`.
CookieHTTPOnly = context.CookieHTTPOnly CookieEncoding = context.CookieEncoding
// CookieEncode is a `CookieOption`.
// Provides encoding functionality when adding a cookie.
// Accepts a `context#CookieEncoder` and sets the cookie's value to the encoded value.
// Users of that is the `context#SetCookie` and `context#SetCookieKV`.
//
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
//
// A shortcut for the `context#CookieEncode`.
CookieEncode = context.CookieEncode
// CookieDecode is a `CookieOption`.
// Provides decoding functionality when retrieving a cookie.
// Accepts a `context#CookieDecoder` and sets the cookie's value to the decoded value before return by the `GetCookie`.
// User of that is the `context#GetCookie`.
//
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
//
// A shortcut for the `context#CookieDecode`.
CookieDecode = context.CookieDecode
// IsErrPath can be used at `context#ReadForm`. // IsErrPath can be used at `context#ReadForm`.
// It reports whether the incoming error is type of `formbinder.ErrPath`, // It reports whether the incoming error is type of `formbinder.ErrPath`,
// which can be ignored when server allows unknown post values to be sent by the client. // which can be ignored when server allows unknown post values to be sent by the client.

View File

@ -13,33 +13,6 @@ const (
DefaultCookieName = "irissessionid" DefaultCookieName = "irissessionid"
) )
// Encoding is the Cookie Encoder/Decoder interface, which can be passed as configuration field
// alternatively to the `Encode` and `Decode` fields.
type Encoding interface {
// 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 set 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(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(cookieName string, cookieValue string, v interface{}) error
}
type ( type (
// Config is the configuration for sessions. Please read it before using sessions. // Config is the configuration for sessions. Please read it before using sessions.
Config struct { Config struct {
@ -66,34 +39,11 @@ type (
// Defaults to false. // Defaults to false.
AllowReclaim bool AllowReclaim bool
// Encode the cookie value if not nil. // Encoding should encodes and decodes
// Should accept as first argument the cookie name (config.Cookie) // authenticated and optionally encrypted cookie values.
// as second argument the server's generated session id.
// Should return the new session id, if error the session id set 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. // Defaults to nil.
Encode func(cookieName string, value interface{}) (string, error) Encoding context.SecureCookie
// Decode the cookie value if not nil.
// Should accept as first argument the cookie name (config.Cookie)
// 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
// Encoding same as Encode and Decode but receives a single instance which
// completes the "CookieEncoder" interface, `Encode` and `Decode` functions.
//
// Defaults to nil.
Encoding Encoding
// 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.
@ -131,10 +81,5 @@ func (c Config) Validate() Config {
} }
} }
if c.Encoding != nil {
c.Encode = c.Encoding.Encode
c.Decode = c.Encoding.Decode
}
return c return c
} }

View File

@ -1,142 +0,0 @@
package sessions
import (
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/kataras/iris/v12/context"
"golang.org/x/net/publicsuffix"
)
var (
// CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
// CookieExpireUnlimited indicates that the cookie doesn't expire.
CookieExpireUnlimited = time.Now().AddDate(24, 10, 10)
)
// GetCookie returns cookie's value by it's name
// returns empty string if nothing was found
func GetCookie(ctx context.Context, name string) string {
c, err := ctx.Request().Cookie(name)
if err != nil {
return ""
}
return c.Value
}
// AddCookie adds a cookie
func AddCookie(ctx context.Context, cookie *http.Cookie, reclaim bool) {
if reclaim {
ctx.Request().AddCookie(cookie)
}
ctx.UpsertCookie(cookie)
}
// RemoveCookie deletes a cookie by it's name/key
// If "purge" is true then it removes the, temp, cookie from the request as well.
func RemoveCookie(ctx context.Context, config Config) {
cookie, err := ctx.Request().Cookie(config.Cookie)
if err != nil {
return
}
cookie.Expires = CookieExpireDelete
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
cookie.MaxAge = -1
cookie.Value = ""
cookie.Path = "/"
cookie.Domain = formatCookieDomain(ctx, config.DisableSubdomainPersistence)
AddCookie(ctx, cookie, config.AllowReclaim)
if config.AllowReclaim {
// delete request's cookie also, which is temporary available.
ctx.Request().Header.Set("Cookie", "")
}
}
// IsValidCookieDomain returns true if the receiver is a valid domain to set
// valid means that is recognised as 'domain' by the browser, so it(the cookie) can be shared with subdomains also
func IsValidCookieDomain(domain string) bool {
if net.IP([]byte(domain)).IsLoopback() {
// for these type of hosts, we can't allow subdomains persistence,
// the web browser doesn't understand the mysubdomain.0.0.0.0 and mysubdomain.127.0.0.1 mysubdomain.32.196.56.181. as scorrectly ubdomains because of the many dots
// so don't set a cookie domain here, let browser handle this
return false
}
dotLen := strings.Count(domain, ".")
if dotLen == 0 {
// we don't have a domain, maybe something like 'localhost', browser doesn't see the .localhost as wildcard subdomain+domain
return false
}
if dotLen >= 3 {
if lastDotIdx := strings.LastIndexByte(domain, '.'); lastDotIdx != -1 {
// chekc the last part, if it's number then propably it's ip
if len(domain) > lastDotIdx+1 {
_, err := strconv.Atoi(domain[lastDotIdx+1:])
if err == nil {
return false
}
}
}
}
return true
}
// func formatCookieDomain(ctx context.Context, disableSubdomainPersistence bool) string {
// if disableSubdomainPersistence {
// return ""
// }
// requestDomain := ctx.Host()
// if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
// requestDomain = requestDomain[0:portIdx]
// }
// if !IsValidCookieDomain(requestDomain) {
// return ""
// }
// // RFC2109, we allow level 1 subdomains, but no further
// // if we have localhost.com , we want the localhost.com.
// // so if we have something like: mysubdomain.localhost.com we want the localhost here
// // if we have mysubsubdomain.mysubdomain.localhost.com we want the .mysubdomain.localhost.com here
// // slow things here, especially the 'replace' but this is a good and understable( I hope) way to get the be able to set cookies from subdomains & domain with 1-level limit
// if dotIdx := strings.IndexByte(requestDomain, '.'); dotIdx > 0 {
// // is mysubdomain.localhost.com || mysubsubdomain.mysubdomain.localhost.com
// if strings.IndexByte(requestDomain[dotIdx+1:], '.') > 0 {
// requestDomain = requestDomain[dotIdx+1:]
// }
// }
// // finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow)
// return "." + requestDomain // . to allow persistence
// }
func formatCookieDomain(ctx context.Context, disableSubdomainPersistence bool) string {
if disableSubdomainPersistence {
return ""
}
host := ctx.Host()
if portIdx := strings.IndexByte(host, ':'); portIdx > 0 {
host = host[0:portIdx]
}
domain, err := publicsuffix.EffectiveTLDPlusOne(host)
if err != nil {
return "." + host
}
return "." + domain
}

View File

@ -2,6 +2,8 @@ package sessions
import ( import (
"time" "time"
"github.com/kataras/iris/v12/context"
) )
// LifeTime controls the session expiration datetime. // LifeTime controls the session expiration datetime.
@ -50,7 +52,7 @@ func (lt *LifeTime) Shift(d time.Duration) {
// ExpireNow reduce the lifetime completely. // ExpireNow reduce the lifetime completely.
func (lt *LifeTime) ExpireNow() { func (lt *LifeTime) ExpireNow() {
lt.Time = CookieExpireDelete lt.Time = context.CookieExpireDelete
if lt.timer != nil { if lt.timer != nil {
lt.timer.Stop() lt.timer.Stop()
} }

View File

@ -21,6 +21,8 @@ func init() {
type Sessions struct { type Sessions struct {
config Config config Config
provider *provider provider *provider
handlerCookieOpts []context.CookieOption // see `Handler`.
} }
// New returns a new fast, feature-rich sessions manager // New returns a new fast, feature-rich sessions manager
@ -38,52 +40,46 @@ func (s *Sessions) UseDatabase(db Database) {
s.provider.RegisterDatabase(db) s.provider.RegisterDatabase(db)
} }
// GetCookieOptions returns any cookie options registered for the `Handler` method.
func (s *Sessions) GetCookieOptions() []context.CookieOption {
return s.handlerCookieOpts
}
// updateCookie gains the ability of updating the session browser cookie to any method which wants to update it // updateCookie gains the ability of updating the session browser cookie to any method which wants to update it
func (s *Sessions) updateCookie(ctx context.Context, sid string, expires time.Duration, options ...context.CookieOption) { func (s *Sessions) updateCookie(ctx context.Context, sid string, expires time.Duration, options ...context.CookieOption) {
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 = "/"
cookie.Domain = formatCookieDomain(ctx, s.config.DisableSubdomainPersistence)
cookie.HttpOnly = true cookie.HttpOnly = true
if !s.config.DisableSubdomainPersistence {
cookie.SameSite = http.SameSiteLaxMode // allow subdomain sharing.
}
// MaxAge=0 means no 'Max-Age' attribute specified. // MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds // MaxAge>0 means Max-Age attribute present and given in seconds
if expires >= 0 { if expires >= 0 {
if expires == 0 { // unlimited life if expires == 0 { // unlimited life
cookie.Expires = CookieExpireUnlimited cookie.Expires = context.CookieExpireUnlimited
} else { // > 0 } else { // > 0
cookie.Expires = time.Now().Add(expires) cookie.Expires = time.Now().Add(expires)
} }
cookie.MaxAge = int(time.Until(cookie.Expires).Seconds()) cookie.MaxAge = int(time.Until(cookie.Expires).Seconds())
} }
// set the cookie to secure if this is a tls wrapped request ctx.UpsertCookie(cookie, options...)
// and the configuration allows it.
if ctx.Request().TLS != nil && s.config.CookieSecureTLS {
cookie.Secure = true
}
// encode the session id cookie client value right before send it.
cookie.Value = s.encodeCookieValue(cookie.Value)
for _, opt := range options {
opt(cookie)
}
AddCookie(ctx, cookie, s.config.AllowReclaim)
} }
// Start creates or retrieves an existing session for the particular request. // Start creates or retrieves an existing session for the particular request.
// Note that `Start` method will not respect configuration's `AllowReclaim`, `DisableSubdomainPersistence`, `CookieSecureTLS`,
// and `Encoding` settings.
// Register sessions as a middleware through the `Handler` method instead,
// which provides automatic resolution of a *sessions.Session input argument
// on MVC and APIContainer as well.
//
// NOTE: Use `app.Use(sess.Handler())` instead, avoid using `Start` manually.
func (s *Sessions) Start(ctx context.Context, cookieOptions ...context.CookieOption) *Session { func (s *Sessions) Start(ctx context.Context, cookieOptions ...context.CookieOption) *Session {
cookieValue := s.decodeCookieValue(GetCookie(ctx, s.config.Cookie)) cookieValue := ctx.GetCookie(s.config.Cookie, cookieOptions...)
if cookieValue == "" { // cookie doesn't exist, let's generate a session and set a cookie. if cookieValue == "" { // cookie doesn't exist, let's generate a session and set a cookie.
sid := s.config.SessionIDGenerator(ctx) sid := s.config.SessionIDGenerator(ctx)
@ -99,13 +95,34 @@ func (s *Sessions) Start(ctx context.Context, cookieOptions ...context.CookieOpt
return s.provider.Read(s, cookieValue, s.config.Expires) return s.provider.Read(s, cookieValue, s.config.Expires)
} }
const contextSessionKey = "iris.session" const sessionContextKey = "iris.session"
// Handler returns a sessions middleware to register on application routes. // Handler returns a sessions middleware to register on application routes.
// To return the request's Session call the `Get(ctx)` package-level function.
//
// Call `Handler()` once per sessions manager.
func (s *Sessions) Handler(cookieOptions ...context.CookieOption) context.Handler { func (s *Sessions) Handler(cookieOptions ...context.CookieOption) context.Handler {
s.handlerCookieOpts = cookieOptions
return func(ctx context.Context) { return func(ctx context.Context) {
session := s.Start(ctx, cookieOptions...) var requestOptions []context.CookieOption
ctx.Values().Set(contextSessionKey, session) if s.config.AllowReclaim {
requestOptions = append(requestOptions, context.CookieAllowReclaim(ctx, s.config.Cookie))
}
if !s.config.DisableSubdomainPersistence {
requestOptions = append(requestOptions, context.CookieAllowSubdomains(ctx, s.config.Cookie))
}
if s.config.CookieSecureTLS {
requestOptions = append(requestOptions, context.CookieSecure(ctx))
}
if s.config.Encoding != nil {
requestOptions = append(requestOptions, context.CookieEncoding(s.config.Encoding, s.config.Cookie))
}
ctx.AddCookieOptions(requestOptions...) // request life-cycle options.
session := s.Start(ctx, cookieOptions...) // this cookie's end-developer's custom options.
ctx.Values().Set(sessionContextKey, session)
ctx.Next() ctx.Next()
} }
} }
@ -116,14 +133,17 @@ func (s *Sessions) Handler(cookieOptions ...context.CookieOption) context.Handle
// The `Sessions.Start` should be called previously, // The `Sessions.Start` should be called previously,
// e.g. register the `Sessions.Handler` as middleware. // e.g. register the `Sessions.Handler` as middleware.
// Then call `Get` package-level function as many times as you want. // Then call `Get` package-level function as many times as you want.
// The `Sessions.Start` can be called more than one time in the same request life cycle as well. // Note: It will return nil if the session got destroyed by the same request.
// If you need to destroy and start a new session in the same request you need to call
// sessions manager's `Start` method after Destroy.
func Get(ctx context.Context) *Session { func Get(ctx context.Context) *Session {
if v := ctx.Values().Get(contextSessionKey); v != nil { if v := ctx.Values().Get(sessionContextKey); v != nil {
if sess, ok := v.(*Session); ok { if sess, ok := v.(*Session); ok {
return sess return sess
} }
} }
// ctx.Application().Logger().Debugf("Sessions: Get: no session found, prior Destroy(ctx) calls in the same request should follow with a Start(ctx) call too")
return nil return nil
} }
@ -144,7 +164,7 @@ func (s *Sessions) ShiftExpiration(ctx context.Context, cookieOptions ...context
// It will return `ErrNotFound` when trying to update expiration on a non-existence or not valid session entry. // It will return `ErrNotFound` when trying to update expiration on a non-existence or not valid session entry.
// It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet. // It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet.
func (s *Sessions) UpdateExpiration(ctx context.Context, expires time.Duration, cookieOptions ...context.CookieOption) error { func (s *Sessions) UpdateExpiration(ctx context.Context, expires time.Duration, cookieOptions ...context.CookieOption) error {
cookieValue := s.decodeCookieValue(GetCookie(ctx, s.config.Cookie)) cookieValue := ctx.GetCookie(s.config.Cookie)
if cookieValue == "" { if cookieValue == "" {
return ErrNotFound return ErrNotFound
} }
@ -172,17 +192,20 @@ func (s *Sessions) OnDestroy(listeners ...DestroyListener) {
} }
} }
// Destroy remove the session data and remove the associated cookie. // Destroy removes the session data, the associated cookie
// and the Context's session value.
// Next calls of `sessions.Get` will occur to a nil Session,
// use `Sessions#Start` method for renewal
// or use the Session's Destroy method which does keep the session entry with its values cleared.
func (s *Sessions) Destroy(ctx context.Context) { func (s *Sessions) Destroy(ctx context.Context) {
cookieValue := GetCookie(ctx, s.config.Cookie) cookieValue := ctx.GetCookie(s.config.Cookie)
// decode the client's cookie value in order to find the server's session id
// to destroy the session data.
cookieValue = s.decodeCookieValue(cookieValue)
if cookieValue == "" { // nothing to destroy if cookieValue == "" { // nothing to destroy
return return
} }
RemoveCookie(ctx, s.config)
ctx.Values().Remove(sessionContextKey)
ctx.RemoveCookie(s.config.Cookie)
s.provider.Destroy(cookieValue) s.provider.Destroy(cookieValue)
} }
@ -204,35 +227,3 @@ func (s *Sessions) DestroyByID(sid string) {
func (s *Sessions) DestroyAll() { func (s *Sessions) DestroyAll() {
s.provider.DestroyAll() s.provider.DestroyAll()
} }
// let's keep these funcs simple, we can do it with two lines but we may add more things in the future.
func (s *Sessions) decodeCookieValue(cookieValue string) string {
if cookieValue == "" {
return ""
}
if decode := s.config.Decode; decode != nil {
var cookieValueDecoded string
err := decode(s.config.Cookie, cookieValue, &cookieValueDecoded)
if err == nil {
cookieValue = cookieValueDecoded
} else {
cookieValue = ""
}
}
return cookieValue
}
func (s *Sessions) encodeCookieValue(cookieValue string) string {
if encode := s.config.Encode; encode != nil {
newVal, err := encode(s.config.Cookie, cookieValue)
if err == nil {
cookieValue = newVal
} else {
cookieValue = ""
}
}
return cookieValue
}

View File

@ -15,14 +15,16 @@ func TestSessions(t *testing.T) {
app := iris.New() app := iris.New()
sess := sessions.New(sessions.Config{Cookie: "mycustomsessionid"}) sess := sessions.New(sessions.Config{Cookie: "mycustomsessionid"})
testSessions(t, sess, app) app.Use(sess.Handler())
testSessions(t, app)
} }
const ( const (
testEnableSubdomain = true testEnableSubdomain = true
) )
func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application) { func testSessions(t *testing.T, app *iris.Application) {
values := map[string]interface{}{ values := map[string]interface{}{
"Name": "iris", "Name": "iris",
"Months": "4", "Months": "4",
@ -30,7 +32,7 @@ func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application)
} }
writeValues := func(ctx context.Context) { writeValues := func(ctx context.Context) {
s := sess.Start(ctx) s := sessions.Get(ctx)
sessValues := s.GetAll() sessValues := s.GetAll()
_, err := ctx.JSON(sessValues) _, err := ctx.JSON(sessValues)
@ -44,7 +46,7 @@ func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application)
} }
app.Post("/set", func(ctx context.Context) { app.Post("/set", func(ctx context.Context) {
s := sess.Start(ctx) s := sessions.Get(ctx)
vals := make(map[string]interface{}) vals := make(map[string]interface{})
if err := ctx.ReadJSON(&vals); err != nil { if err := ctx.ReadJSON(&vals); err != nil {
t.Fatalf("Cannot read JSON. Trace %s", err.Error()) t.Fatalf("Cannot read JSON. Trace %s", err.Error())
@ -59,26 +61,38 @@ func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application)
}) })
app.Get("/clear", func(ctx context.Context) { app.Get("/clear", func(ctx context.Context) {
sess.Start(ctx).Clear() sessions.Get(ctx).Clear()
writeValues(ctx) writeValues(ctx)
}) })
app.Get("/destroy", func(ctx context.Context) { app.Get("/destroy", func(ctx context.Context) {
sess.Destroy(ctx) session := sessions.Get(ctx)
writeValues(ctx) if session.IsNew() {
t.Fatal("expected session not to be nil on destroy")
}
session.Man.Destroy(ctx)
if sessions.Get(ctx) != nil {
t.Fatal("expected session inside Context to be nil after Manager's Destroy call")
}
ctx.JSON(struct{}{})
// the cookie and all values should be empty // the cookie and all values should be empty
}) })
// request cookie should be empty // cookie should be new.
app.Get("/after_destroy", func(ctx context.Context) { app.Get("/after_destroy_renew", func(ctx context.Context) {
isNew := sessions.Get(ctx).IsNew()
ctx.Writef("%v", isNew)
}) })
app.Get("/multi_start_set_get", func(ctx context.Context) { app.Get("/multi_start_set_get", func(ctx context.Context) {
s := sess.Start(ctx) s := sessions.Get(ctx)
s.Set("key", "value") s.Set("key", "value")
ctx.Next() ctx.Next()
}, func(ctx context.Context) { }, func(ctx context.Context) {
s := sess.Start(ctx) s := sessions.Get(ctx)
_, err := ctx.Writef(s.GetString("key")) _, err := ctx.Writef(s.GetString("key"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -98,12 +112,13 @@ func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application)
// test destroy which also clears first // test destroy which also clears first
d := e.GET("/destroy").Expect().Status(iris.StatusOK) d := e.GET("/destroy").Expect().Status(iris.StatusOK)
d.JSON().Object().Empty() d.JSON().Object().Empty()
// This removed: d.Cookies().Empty(). Reason:
// httpexpect counts the cookies set or deleted at the response time, but cookie is not removed, to be really removed needs to SetExpire(now-1second) so, d = e.GET("/after_destroy_renew").Expect().Status(iris.StatusOK)
// test if the cookies removed on the next request, like the browser's behavior. d.Body().Equal("true")
e.GET("/after_destroy").Expect().Status(iris.StatusOK).Cookies().Empty() d.Cookies().NotEmpty()
// set and clear again // set and clear again
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK)
e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty() e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty()
// test start on the same request but more than one times // test start on the same request but more than one times