mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
enhanced cookie security and management
Former-commit-id: a97b0b33e87749a2e8c32e63269fcc60fa326ff3
This commit is contained in:
parent
d5f1649895
commit
50b18c7515
13
HISTORY.md
13
HISTORY.md
|
@ -371,12 +371,13 @@ Other Improvements:
|
|||
|
||||
![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` 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.
|
||||
|
||||
- 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.
|
||||
|
||||
|
@ -396,8 +397,16 @@ Other Improvements:
|
|||
|
||||
- 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:
|
||||
|
||||
- `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.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
|
||||
|
@ -421,6 +430,8 @@ New Context Methods:
|
|||
|
||||
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.
|
||||
- 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).
|
||||
|
|
|
@ -74,16 +74,14 @@ var sessionsManager *sessions.Sessions
|
|||
func init() {
|
||||
// attach a session manager
|
||||
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")
|
||||
hashKey := securecookie.GenerateRandomKey(64)
|
||||
blockKey := securecookie.GenerateRandomKey(32)
|
||||
secureCookie := securecookie.New(hashKey, blockKey)
|
||||
|
||||
sessionsManager = sessions.New(sessions.Config{
|
||||
Cookie: cookieName,
|
||||
Encode: secureCookie.Encode,
|
||||
Decode: secureCookie.Decode,
|
||||
Encoding: secureCookie,
|
||||
AllowReclaim: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -13,16 +13,16 @@ func TestCookiesBasic(t *testing.T) {
|
|||
|
||||
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.Cookie(cookieName).Value().Equal(cookieValue) // validate cookie's existence, it should be there now.
|
||||
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.Body().Equal(cookieValue)
|
||||
|
||||
// Test Remove A Cookie.
|
||||
// Test remove a Cookie.
|
||||
t3 := e.DELETE(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK)
|
||||
t3.Body().Contains(cookieName)
|
||||
|
||||
|
|
|
@ -11,49 +11,62 @@ import (
|
|||
"github.com/gorilla/securecookie"
|
||||
)
|
||||
|
||||
var (
|
||||
// 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")
|
||||
sc = securecookie.New(hashKey, blockKey)
|
||||
)
|
||||
func main() {
|
||||
app := newApp()
|
||||
// http://localhost:8080/cookies/name/value
|
||||
// http://localhost:8080/cookies/name
|
||||
// http://localhost:8080/cookies/remove/name
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func newApp() *iris.Application {
|
||||
app := iris.New()
|
||||
|
||||
r := app.Party("/cookies")
|
||||
{
|
||||
r.Use(useSecureCookies())
|
||||
|
||||
// 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")
|
||||
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)
|
||||
})
|
||||
|
||||
// Retrieve A Cookie.
|
||||
app.Get("/cookies/{name}", func(ctx iris.Context) {
|
||||
r.Get("/{name}", func(ctx iris.Context) {
|
||||
name := ctx.Params().Get("name")
|
||||
|
||||
value := ctx.GetCookie(name, iris.CookieDecode(sc.Decode)) // <--
|
||||
value := ctx.GetCookie(name)
|
||||
|
||||
ctx.WriteString(value)
|
||||
})
|
||||
|
||||
// Delete A Cookie.
|
||||
app.Delete("/cookies/{name}", func(ctx iris.Context) {
|
||||
r.Get("/remove/{name}", func(ctx iris.Context) {
|
||||
name := ctx.Params().Get("name")
|
||||
|
||||
ctx.RemoveCookie(name) // <--
|
||||
ctx.RemoveCookie(name)
|
||||
|
||||
ctx.Writef("cookie %s removed", name)
|
||||
})
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := newApp()
|
||||
app.Listen(":8080")
|
||||
func useSecureCookies() iris.Handler {
|
||||
var (
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,25 +7,25 @@ import (
|
|||
"github.com/kataras/iris/v12/httptest"
|
||||
)
|
||||
|
||||
func TestCookiesBasic(t *testing.T) {
|
||||
func TestSecureCookie(t *testing.T) {
|
||||
app := newApp()
|
||||
e := httptest.New(t, app, httptest.URL("http://example.com"))
|
||||
|
||||
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)
|
||||
// note that this will not work because it doesn't always returns the same value:
|
||||
// cookieValueEncoded, _ := sc.Encode(cookieName, cookieValue)
|
||||
t1.Cookie(cookieName).Value().NotEqual(cookieValue) // validate cookie's existence and value is not on its raw form.
|
||||
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.Body().Equal(cookieValue)
|
||||
|
||||
// Test Remove A Cookie.
|
||||
t3 := e.DELETE(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK)
|
||||
// Test remove a Cookie.
|
||||
t3 := e.GET(fmt.Sprintf("/cookies/remove/%s", cookieName)).Expect().Status(httptest.StatusOK)
|
||||
t3.Body().Contains(cookieName)
|
||||
|
||||
t4 := e.GET(fmt.Sprintf("/cookies/%s", cookieName)).Expect().Status(httptest.StatusOK)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
![tunneling_screenshot.png](tunneling_screenshot.png)
|
|
@ -14,7 +14,7 @@ func main() {
|
|||
ctx.Application().ConfigurationReadOnly().GetVHost())
|
||||
})
|
||||
|
||||
app.Listen(":8080", iris.WithTunneling)
|
||||
app.Listen(":8080", iris.WithTunneling, iris.WithLogLevel("debug"))
|
||||
|
||||
/* The full configuration can be set as:
|
||||
app.Listen(":8080", iris.WithConfiguration(
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 164 KiB |
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
"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"
|
||||
)
|
||||
|
||||
|
|
|
@ -20,14 +20,13 @@ func newApp() *iris.Application {
|
|||
cookieName := "_session_id"
|
||||
// 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)
|
||||
hashKey := securecookie.GenerateRandomKey(64)
|
||||
blockKey := securecookie.GenerateRandomKey(32)
|
||||
s := securecookie.New(hashKey, blockKey)
|
||||
|
||||
mySessions := sessions.New(sessions.Config{
|
||||
Cookie: cookieName,
|
||||
Encode: secureCookie.Encode,
|
||||
Decode: secureCookie.Decode,
|
||||
Encoding: s,
|
||||
AllowReclaim: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -879,7 +879,7 @@ type Configuration struct {
|
|||
LanguageContextKey string `json:"languageContextKey,omitempty" yaml:"LanguageContextKey" toml:"LanguageContextKey"`
|
||||
// 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")`.
|
||||
// Defauls to "iris.api.version".
|
||||
// Defaults to "iris.api.version".
|
||||
VersionContextKey string `json:"versionContextKey" yaml:"VersionContextKey" toml:"VersionContextKey"`
|
||||
// GetViewLayoutContextKey is the key of the context's user values' key
|
||||
// which is being used to set the template
|
||||
|
|
|
@ -36,6 +36,7 @@ import (
|
|||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"golang.org/x/time/rate"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
@ -984,6 +985,25 @@ type Context interface {
|
|||
// | 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.
|
||||
// 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.
|
||||
// It reports whether the cookie is new (true) or an existing one was updated (false).
|
||||
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).
|
||||
//
|
||||
// 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 |
|
||||
// +------------------------------------------------------------+
|
||||
|
||||
// Set of Cookie actions for `CookieOption`.
|
||||
const (
|
||||
OpCookieGet uint8 = iota
|
||||
OpCookieSet
|
||||
OpCookieDel
|
||||
)
|
||||
|
||||
// CookieOption is the type of function that is accepted on
|
||||
// context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie`
|
||||
// as their (last) variadic input argument to amend the end cookie's form.
|
||||
//
|
||||
// Any custom or builtin `CookieOption` is valid,
|
||||
// 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`.
|
||||
// Use it to change the cookie's Path field.
|
||||
func CookiePath(path string) CookieOption {
|
||||
return func(c *http.Cookie) {
|
||||
c.Path = path
|
||||
// findCookieAgainst reports whether the "cookie.Name" is in the list of "cookieNames".
|
||||
// Notes:
|
||||
// If "cookieNames" slice is empty then it returns true,
|
||||
// If "cookie.Name" is empty then it returns false.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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.
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
// CookieCleanPath is a `CookieOption`.
|
||||
// Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`.
|
||||
func CookieCleanPath(c *http.Cookie) {
|
||||
c.Path = ""
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
c.Expires = time.Now().Add(durFromNow)
|
||||
c.MaxAge = int(durFromNow.Seconds())
|
||||
// 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`.
|
||||
// Use it to set the cookie's HttpOnly field to false or true.
|
||||
// HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`.
|
||||
// See `CookieSecure` too.
|
||||
func CookieHTTPOnly(httpOnly bool) CookieOption {
|
||||
return func(c *http.Cookie) {
|
||||
return func(c *http.Cookie, op uint8) {
|
||||
if op == OpCookieSet {
|
||||
c.HttpOnly = httpOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
// CookieEncoder should encode the cookie value.
|
||||
// CookiePath is a `CookieOption`.
|
||||
// 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
|
||||
// and as second argument the cookie value ptr.
|
||||
// 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.
|
||||
// You either need to provide exactly that amount or you derive the key from what you type in.
|
||||
//
|
||||
// See `CookieDecoder` too.
|
||||
CookieEncoder func(cookieName string, value interface{}) (string, error)
|
||||
// CookieDecoder should decode the cookie value.
|
||||
// See `Decode` too.
|
||||
Encode(cookieName string, cookieValue interface{}) (string, error)
|
||||
// Decode should decode the cookie value.
|
||||
// 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.
|
||||
// Should return a decoded value or an empty one if decode operation failed.
|
||||
|
@ -4797,39 +4932,100 @@ type (
|
|||
// 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.
|
||||
//
|
||||
// See `CookieEncoder` too.
|
||||
CookieDecoder func(cookieName string, cookieValue string, v interface{}) error
|
||||
)
|
||||
// See `Encode` too.
|
||||
Decode(cookieName string, cookieValue string, cookieValuePtr interface{}) error
|
||||
}
|
||||
|
||||
// CookieEncode is a `CookieOption`.
|
||||
// Provides encoding functionality when adding a cookie.
|
||||
// Accepts a `CookieEncoder` and sets the cookie's value to the encoded value.
|
||||
// Users of that is the `SetCookie` and `SetCookieKV`.
|
||||
// CookieEncoding accepts a value which implements `Encode` and `Decode` methods.
|
||||
// It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods.
|
||||
// And on `Context.GetCookie` method it calls its `Decode`.
|
||||
// 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
|
||||
func CookieEncode(encode CookieEncoder) CookieOption {
|
||||
return func(c *http.Cookie) {
|
||||
newVal, err := encode(c.Name, c.Value)
|
||||
func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption {
|
||||
return func(c *http.Cookie, op uint8) {
|
||||
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 {
|
||||
c.Value = ""
|
||||
} else {
|
||||
c.Value = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CookieDecode is a `CookieOption`.
|
||||
// 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 {
|
||||
return
|
||||
case OpCookieGet:
|
||||
// Should decode, it's a read from the client operation.
|
||||
if err := encoding.Decode(c.Name, c.Value, &c.Value); err != nil {
|
||||
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.
|
||||
|
@ -4837,29 +5033,22 @@ func CookieDecode(decode CookieDecoder) CookieOption {
|
|||
//
|
||||
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/basic
|
||||
func (ctx *context) SetCookie(cookie *http.Cookie, options ...CookieOption) {
|
||||
cookie.SameSite = GetSameSite(ctx)
|
||||
|
||||
for _, opt := range options {
|
||||
opt(cookie)
|
||||
}
|
||||
|
||||
ctx.applyCookieOptions(cookie, OpCookieSet, options)
|
||||
http.SetCookie(ctx.writer, cookie)
|
||||
}
|
||||
|
||||
const setCookieHeaderKey = "Set-Cookie"
|
||||
|
||||
// UpsertCookie adds a cookie to the response like `SetCookie` does
|
||||
// but it will also perform a replacement of the cookie
|
||||
// if already set by a previous `SetCookie` call.
|
||||
// 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 {
|
||||
cookie.SameSite = GetSameSite(ctx)
|
||||
|
||||
for _, opt := range options {
|
||||
opt(cookie)
|
||||
}
|
||||
ctx.applyCookieOptions(cookie, OpCookieSet, options)
|
||||
|
||||
header := ctx.ResponseWriter().Header()
|
||||
|
||||
if cookies := header["Set-Cookie"]; len(cookies) > 0 {
|
||||
if cookies := header[setCookieHeaderKey]; len(cookies) > 0 {
|
||||
s := cookie.Name + "=" // name=?value
|
||||
for i, c := range cookies {
|
||||
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
|
||||
// (e.g. UpdateExpiration, see https://github.com/kataras/iris/issues/1485).
|
||||
cookies[i] = cookie.String()
|
||||
header["Set-Cookie"] = cookies
|
||||
header[setCookieHeaderKey] = cookies
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header.Add("Set-Cookie", cookie.String())
|
||||
header.Add(setCookieHeaderKey, cookie.String())
|
||||
return true
|
||||
}
|
||||
|
||||
const sameSiteContextKey = "iris.cookie_same_site"
|
||||
|
||||
// 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.
|
||||
// SetCookieKVExpiration is 365 days by-default
|
||||
// you can change it or simple, use the SetCookie for more control.
|
||||
//
|
||||
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
|
||||
func (ctx *context) SetSameSite(sameSite http.SameSite) {
|
||||
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
|
||||
}
|
||||
// See CookieExpires` for more.
|
||||
var SetCookieKVExpiration = time.Duration(8760) * time.Hour
|
||||
|
||||
// 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.Value = url.QueryEscape(value)
|
||||
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.MaxAge = int(SetCookieKVExpiration.Seconds())
|
||||
c.MaxAge = int(time.Until(c.Expires).Seconds())
|
||||
|
||||
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
|
||||
func (ctx *context) GetCookie(name string, options ...CookieOption) string {
|
||||
cookie, err := ctx.request.Cookie(name)
|
||||
c, err := ctx.request.Cookie(name)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(cookie)
|
||||
}
|
||||
ctx.applyCookieOptions(c, OpCookieGet, options)
|
||||
|
||||
value, _ := url.QueryUnescape(cookie.Value)
|
||||
value, _ := url.QueryUnescape(c.Value)
|
||||
return value
|
||||
}
|
||||
|
||||
// SetCookieKVExpiration is 365 days by-default
|
||||
// you can change it or simple, use the SetCookie for more control.
|
||||
//
|
||||
// See `SetCookieKVExpiration` and `CookieExpires` for more.
|
||||
var SetCookieKVExpiration = time.Duration(8760) * time.Hour
|
||||
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 does expires after 24 years.
|
||||
CookieExpireUnlimited = time.Now().AddDate(24, 10, 10)
|
||||
)
|
||||
|
||||
// RemoveCookie deletes a cookie by its name and path = "/".
|
||||
// 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.Path = "/" // if user wants to change it, use of the CookieOption `CookiePath` is required if not `ctx.SetCookie`.
|
||||
c.HttpOnly = true
|
||||
|
||||
// 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 = exp
|
||||
c.Expires = CookieExpireDelete
|
||||
c.MaxAge = -1
|
||||
ctx.SetCookie(c, options...)
|
||||
// delete request's cookie also, which is temporary available.
|
||||
ctx.request.Header.Set("Cookie", "")
|
||||
|
||||
ctx.applyCookieOptions(c, OpCookieDel, options)
|
||||
http.SetCookie(ctx.writer, c)
|
||||
}
|
||||
|
||||
// VisitAllCookies takes a visitor function which is called
|
||||
|
|
64
iris.go
64
iris.go
|
@ -493,6 +493,41 @@ var (
|
|||
//
|
||||
// A shortcut of the `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`.
|
||||
// Use it to change the cookie's Path field.
|
||||
//
|
||||
|
@ -508,30 +543,13 @@ var (
|
|||
//
|
||||
// A shortcut for the `context#CookieExpires`.
|
||||
CookieExpires = context.CookieExpires
|
||||
// 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`.
|
||||
// CookieEncoding accepts a value which implements `Encode` and `Decode` methods.
|
||||
// It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods.
|
||||
// And on `Context.GetCookie` method it calls its `Decode`.
|
||||
//
|
||||
// A shortcut for the `context#CookieHTTPOnly`.
|
||||
CookieHTTPOnly = context.CookieHTTPOnly
|
||||
// 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
|
||||
// A shortcut for the `context#CookieEncoding`.
|
||||
CookieEncoding = context.CookieEncoding
|
||||
|
||||
// IsErrPath can be used at `context#ReadForm`.
|
||||
// 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.
|
||||
|
|
|
@ -13,33 +13,6 @@ const (
|
|||
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 (
|
||||
// Config is the configuration for sessions. Please read it before using sessions.
|
||||
Config struct {
|
||||
|
@ -66,34 +39,11 @@ type (
|
|||
// Defaults to false.
|
||||
AllowReclaim bool
|
||||
|
||||
// Encode the cookie value if not nil.
|
||||
// Should accept as first argument the cookie name (config.Cookie)
|
||||
// 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.
|
||||
// Encoding should encodes and decodes
|
||||
// authenticated and optionally encrypted cookie values.
|
||||
//
|
||||
// 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.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
|
||||
Encoding context.SecureCookie
|
||||
|
||||
// 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.
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -2,6 +2,8 @@ package sessions
|
|||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12/context"
|
||||
)
|
||||
|
||||
// LifeTime controls the session expiration datetime.
|
||||
|
@ -50,7 +52,7 @@ func (lt *LifeTime) Shift(d time.Duration) {
|
|||
|
||||
// ExpireNow reduce the lifetime completely.
|
||||
func (lt *LifeTime) ExpireNow() {
|
||||
lt.Time = CookieExpireDelete
|
||||
lt.Time = context.CookieExpireDelete
|
||||
if lt.timer != nil {
|
||||
lt.timer.Stop()
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ func init() {
|
|||
type Sessions struct {
|
||||
config Config
|
||||
provider *provider
|
||||
|
||||
handlerCookieOpts []context.CookieOption // see `Handler`.
|
||||
}
|
||||
|
||||
// New returns a new fast, feature-rich sessions manager
|
||||
|
@ -38,52 +40,46 @@ func (s *Sessions) UseDatabase(db Database) {
|
|||
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
|
||||
func (s *Sessions) updateCookie(ctx context.Context, sid string, expires time.Duration, options ...context.CookieOption) {
|
||||
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 = "/"
|
||||
cookie.Domain = formatCookieDomain(ctx, s.config.DisableSubdomainPersistence)
|
||||
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 delete cookie now, equivalently 'Max-Age: 0'
|
||||
// MaxAge>0 means Max-Age attribute present and given in seconds
|
||||
if expires >= 0 {
|
||||
if expires == 0 { // unlimited life
|
||||
cookie.Expires = CookieExpireUnlimited
|
||||
cookie.Expires = context.CookieExpireUnlimited
|
||||
} else { // > 0
|
||||
cookie.Expires = time.Now().Add(expires)
|
||||
}
|
||||
cookie.MaxAge = int(time.Until(cookie.Expires).Seconds())
|
||||
}
|
||||
|
||||
// set the cookie to secure if this is a tls wrapped request
|
||||
// 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)
|
||||
ctx.UpsertCookie(cookie, options...)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
const contextSessionKey = "iris.session"
|
||||
const sessionContextKey = "iris.session"
|
||||
|
||||
// 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 {
|
||||
s.handlerCookieOpts = cookieOptions
|
||||
|
||||
return func(ctx context.Context) {
|
||||
session := s.Start(ctx, cookieOptions...)
|
||||
ctx.Values().Set(contextSessionKey, session)
|
||||
var requestOptions []context.CookieOption
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -116,14 +133,17 @@ func (s *Sessions) Handler(cookieOptions ...context.CookieOption) context.Handle
|
|||
// The `Sessions.Start` should be called previously,
|
||||
// e.g. register the `Sessions.Handler` as middleware.
|
||||
// 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 {
|
||||
if v := ctx.Values().Get(contextSessionKey); v != nil {
|
||||
if v := ctx.Values().Get(sessionContextKey); v != nil {
|
||||
if sess, ok := v.(*Session); ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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 `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 {
|
||||
cookieValue := s.decodeCookieValue(GetCookie(ctx, s.config.Cookie))
|
||||
cookieValue := ctx.GetCookie(s.config.Cookie)
|
||||
if cookieValue == "" {
|
||||
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) {
|
||||
cookieValue := GetCookie(ctx, 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)
|
||||
cookieValue := ctx.GetCookie(s.config.Cookie)
|
||||
if cookieValue == "" { // nothing to destroy
|
||||
return
|
||||
}
|
||||
RemoveCookie(ctx, s.config)
|
||||
|
||||
ctx.Values().Remove(sessionContextKey)
|
||||
|
||||
ctx.RemoveCookie(s.config.Cookie)
|
||||
s.provider.Destroy(cookieValue)
|
||||
}
|
||||
|
||||
|
@ -204,35 +227,3 @@ func (s *Sessions) DestroyByID(sid string) {
|
|||
func (s *Sessions) 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
|
||||
}
|
||||
|
|
|
@ -15,14 +15,16 @@ func TestSessions(t *testing.T) {
|
|||
app := iris.New()
|
||||
|
||||
sess := sessions.New(sessions.Config{Cookie: "mycustomsessionid"})
|
||||
testSessions(t, sess, app)
|
||||
app.Use(sess.Handler())
|
||||
|
||||
testSessions(t, app)
|
||||
}
|
||||
|
||||
const (
|
||||
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{}{
|
||||
"Name": "iris",
|
||||
"Months": "4",
|
||||
|
@ -30,7 +32,7 @@ func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application)
|
|||
}
|
||||
|
||||
writeValues := func(ctx context.Context) {
|
||||
s := sess.Start(ctx)
|
||||
s := sessions.Get(ctx)
|
||||
sessValues := s.GetAll()
|
||||
|
||||
_, 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) {
|
||||
s := sess.Start(ctx)
|
||||
s := sessions.Get(ctx)
|
||||
vals := make(map[string]interface{})
|
||||
if err := ctx.ReadJSON(&vals); err != nil {
|
||||
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) {
|
||||
sess.Start(ctx).Clear()
|
||||
sessions.Get(ctx).Clear()
|
||||
writeValues(ctx)
|
||||
})
|
||||
|
||||
app.Get("/destroy", func(ctx context.Context) {
|
||||
sess.Destroy(ctx)
|
||||
writeValues(ctx)
|
||||
session := sessions.Get(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
|
||||
})
|
||||
|
||||
// request cookie should be empty
|
||||
app.Get("/after_destroy", func(ctx context.Context) {
|
||||
// cookie should be new.
|
||||
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) {
|
||||
s := sess.Start(ctx)
|
||||
s := sessions.Get(ctx)
|
||||
s.Set("key", "value")
|
||||
ctx.Next()
|
||||
}, func(ctx context.Context) {
|
||||
s := sess.Start(ctx)
|
||||
s := sessions.Get(ctx)
|
||||
_, err := ctx.Writef(s.GetString("key"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -98,12 +112,13 @@ func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application)
|
|||
// 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 set 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()
|
||||
|
||||
d = e.GET("/after_destroy_renew").Expect().Status(iris.StatusOK)
|
||||
d.Body().Equal("true")
|
||||
d.Cookies().NotEmpty()
|
||||
|
||||
// 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()
|
||||
|
||||
// test start on the same request but more than one times
|
||||
|
|
Loading…
Reference in New Issue
Block a user