mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
improvements on cookie options
Former-commit-id: f1d5cfc88a33077a9359eaa25b6a20265f5632b5
This commit is contained in:
parent
50b18c7515
commit
221f026491
|
@ -372,6 +372,8 @@ 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.
|
||||
- `Context.RemoveCookie` removes also the Request's cookies of the same request lifecycle when `iris.CookieAllowReclaim` is set to cookie options, [example](https://github.com/kataras/iris/tree/master/_examples/cookies/options).
|
||||
|
||||
- `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))`.
|
||||
|
||||
|
@ -405,6 +407,7 @@ New Package-level Variables:
|
|||
|
||||
New Context Methods:
|
||||
|
||||
- `Context.GetDomain() string` returns the domain.
|
||||
- `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)
|
||||
|
@ -423,7 +426,6 @@ New Context Methods:
|
|||
- `Context.ReadProtobuf(ptr)` binds request body to a proto message
|
||||
- `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
|
||||
- `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
|
||||
- `Context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too)
|
||||
- `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead
|
||||
- `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)`
|
||||
- `Context.Controller() reflect.Value` returns the current MVC Controller value.
|
||||
|
|
|
@ -137,6 +137,7 @@
|
|||
* [Manage Permissions](permissions/main.go)
|
||||
* Cookies
|
||||
* [Basic](cookies/basic/main.go)
|
||||
* [Options](cookies/options/main.go)
|
||||
* [Encode/Decode (with `securecookie`)](cookies/securecookie/main.go)
|
||||
* Sessions
|
||||
* [Overview: Config](sessions/overview/main.go)
|
||||
|
|
76
_examples/cookies/options/main.go
Normal file
76
_examples/cookies/options/main.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := newApp()
|
||||
|
||||
// http://localhost:8080/set/name1/value1
|
||||
// http://localhost:8080/get/name1
|
||||
// http://localhost:8080/remove/name1
|
||||
app.Listen(":8080", iris.WithLogLevel("debug"))
|
||||
}
|
||||
|
||||
func newApp() *iris.Application {
|
||||
app := iris.New()
|
||||
app.Use(withCookieOptions)
|
||||
|
||||
app.Get("/set/{name}/{value}", setCookie)
|
||||
app.Get("/get/{name}", getCookie)
|
||||
app.Get("/remove/{name}", removeCookie)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func withCookieOptions(ctx iris.Context) {
|
||||
// Register cookie options for request-lifecycle.
|
||||
// To register per cookie, just add the CookieOption
|
||||
// on the last variadic input argument of
|
||||
// SetCookie, SetCookieKV, UpsertCookie, RemoveCookie
|
||||
// and GetCookie Context methods.
|
||||
//
|
||||
// * CookieAllowReclaim
|
||||
// * CookieAllowSubdomains
|
||||
// * CookieSecure
|
||||
// * CookieHTTPOnly
|
||||
// * CookieSameSite
|
||||
// * CookiePath
|
||||
// * CookieCleanPath
|
||||
// * CookieExpires
|
||||
// * CookieEncoding
|
||||
ctx.AddCookieOptions(iris.CookieAllowReclaim())
|
||||
ctx.Next()
|
||||
}
|
||||
|
||||
func setCookie(ctx iris.Context) {
|
||||
name := ctx.Params().Get("name")
|
||||
value := ctx.Params().Get("value")
|
||||
|
||||
ctx.SetCookieKV(name, value)
|
||||
|
||||
// By-default net/http does not remove or set the Cookie on the Request object.
|
||||
//
|
||||
// With the `CookieAllowReclaim` option, whenever you set or remove a cookie
|
||||
// it will be also reflected in the Request object immediately (of the same request lifecycle)
|
||||
// therefore, any of the next handlers in the chain are not holding the old value.
|
||||
valueIsAvailableInRequestObject := ctx.GetCookie(name)
|
||||
ctx.Writef("cookie %s=%s", name, valueIsAvailableInRequestObject)
|
||||
}
|
||||
|
||||
func getCookie(ctx iris.Context) {
|
||||
name := ctx.Params().Get("name")
|
||||
|
||||
value := ctx.GetCookie(name)
|
||||
ctx.WriteString(value)
|
||||
}
|
||||
|
||||
func removeCookie(ctx iris.Context) {
|
||||
name := ctx.Params().Get("name")
|
||||
|
||||
ctx.RemoveCookie(name)
|
||||
|
||||
removedFromRequestObject := ctx.GetCookie(name) // CookieAllowReclaim feature.
|
||||
ctx.Writef("cookie %s removed, value should be empty=%s", name, removedFromRequestObject)
|
||||
}
|
32
_examples/cookies/options/main_test.go
Normal file
32
_examples/cookies/options/main_test.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/v12/httptest"
|
||||
)
|
||||
|
||||
func TestCookieOptions(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.
|
||||
t1 := e.GET(fmt.Sprintf("/set/%s/%s", cookieName, cookieValue)).Expect().Status(httptest.StatusOK)
|
||||
t1.Cookie(cookieName).Value().Equal(cookieValue)
|
||||
t1.Body().Contains(fmt.Sprintf("%s=%s", cookieName, cookieValue))
|
||||
|
||||
// Test retrieve a Cookie.
|
||||
t2 := e.GET(fmt.Sprintf("/get/%s", cookieName)).Expect().Status(httptest.StatusOK)
|
||||
t2.Body().Equal(cookieValue)
|
||||
|
||||
// Test remove a Cookie.
|
||||
t3 := e.GET(fmt.Sprintf("/remove/%s", cookieName)).Expect().Status(httptest.StatusOK)
|
||||
t3.Body().Contains(fmt.Sprintf("cookie %s removed, value should be empty=%s", cookieName, ""))
|
||||
|
||||
t4 := e.GET(fmt.Sprintf("/get/%s", cookieName)).Expect().Status(httptest.StatusOK)
|
||||
t4.Cookies().Empty()
|
||||
t4.Body().Empty()
|
||||
}
|
|
@ -378,6 +378,8 @@ type Context interface {
|
|||
RemoteAddr() string
|
||||
// GetHeader returns the request header's value based on its name.
|
||||
GetHeader(name string) string
|
||||
// GetDomain resolves and returns the server's domain.
|
||||
GetDomain() string
|
||||
// IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest)
|
||||
//
|
||||
// There is no a 100% way of knowing that a request was made via Ajax.
|
||||
|
@ -992,12 +994,15 @@ type Context interface {
|
|||
// See `ClearCookieOptions` too.
|
||||
//
|
||||
// Available builtin Cookie options are:
|
||||
// * CookieSameSite
|
||||
// * CookiePath
|
||||
// * CookieCleanPath
|
||||
// * CookieExpires
|
||||
// * CookieHTTPOnly
|
||||
// * CookieEncoding
|
||||
// * CookieAllowReclaim
|
||||
// * CookieAllowSubdomains
|
||||
// * CookieSecure
|
||||
// * CookieHTTPOnly
|
||||
// * CookieSameSite
|
||||
// * CookiePath
|
||||
// * CookieCleanPath
|
||||
// * CookieExpires
|
||||
// * CookieEncoding
|
||||
//
|
||||
// Example at: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
|
||||
AddCookieOptions(options ...CookieOption)
|
||||
|
@ -1878,6 +1883,20 @@ func (ctx *context) GetHeader(name string) string {
|
|||
return ctx.request.Header.Get(name)
|
||||
}
|
||||
|
||||
// GetDomain resolves and returns the server's domain.
|
||||
func (ctx *context) GetDomain() string {
|
||||
host := ctx.Host()
|
||||
if portIdx := strings.IndexByte(host, ':'); portIdx > 0 {
|
||||
host = host[0:portIdx]
|
||||
}
|
||||
|
||||
if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
|
||||
host = domain
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest)
|
||||
//
|
||||
// There is no a 100% way of knowing that a request was made via Ajax.
|
||||
|
@ -4755,18 +4774,16 @@ const (
|
|||
|
||||
// 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.
|
||||
// as their (last) variadic input argument to amend the to-be-sent cookie.
|
||||
//
|
||||
// Any custom or builtin `CookieOption` is valid,
|
||||
// see `CookiePath`, `CookieCleanPath`, `CookieExpires` and `CookieHTTPOnly` for more.
|
||||
// The "op" is the operation code, 0 is GET, 1 is SET and 2 is REMOVE.
|
||||
type CookieOption func(c *http.Cookie, op uint8)
|
||||
type CookieOption func(ctx Context, c *http.Cookie, op uint8)
|
||||
|
||||
// findCookieAgainst reports whether the "cookie.Name" is in the list of "cookieNames".
|
||||
// CookieIncluded 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 {
|
||||
func CookieIncluded(cookie *http.Cookie, cookieNames []string) bool {
|
||||
if cookie.Name == "" {
|
||||
return false
|
||||
}
|
||||
|
@ -4784,25 +4801,53 @@ func findCookieAgainst(cookie *http.Cookie, cookieNames []string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
|
||||
|
||||
func sanitizeCookieName(n string) string {
|
||||
return cookieNameSanitizer.Replace(n)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func CookieAllowReclaim(cookieNames ...string) CookieOption {
|
||||
return func(ctx Context, c *http.Cookie, op uint8) {
|
||||
if op == OpCookieGet {
|
||||
return
|
||||
}
|
||||
|
||||
if !findCookieAgainst(c, cookieNames) {
|
||||
if !CookieIncluded(c, cookieNames) {
|
||||
return
|
||||
}
|
||||
|
||||
switch op {
|
||||
case OpCookieSet:
|
||||
// perform upsert on request cookies or is it too much and not worth the cost?
|
||||
ctx.Request().AddCookie(c)
|
||||
case OpCookieDel:
|
||||
// TODO: delete only this c.Name.
|
||||
ctx.Request().Header.Set("Cookie", "")
|
||||
header := ctx.Request().Header
|
||||
|
||||
if cookiesLine := header.Get("Cookie"); cookiesLine != "" {
|
||||
if cookies := strings.Split(cookiesLine, "; "); len(cookies) > 1 {
|
||||
// more than one cookie here.
|
||||
// select that one and remove it.
|
||||
name := sanitizeCookieName(c.Name)
|
||||
|
||||
for _, nameValue := range cookies {
|
||||
if strings.HasPrefix(nameValue, name) {
|
||||
cookiesLine = strings.Replace(cookiesLine, "; "+nameValue, "", 1)
|
||||
// current cookiesLine: myapp_session_id=5ccf4e89-8d0e-4ed6-9f4c-6746d7c5e2ee; key1=value1
|
||||
// found nameValue: key1=value1
|
||||
// new cookiesLine: myapp_session_id=5ccf4e89-8d0e-4ed6-9f4c-6746d7c5e2ee
|
||||
header.Set("Cookie", cookiesLine)
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
header.Del("Cookie")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4812,28 +4857,17 @@ func CookieAllowReclaim(ctx Context, cookieNames ...string) CookieOption {
|
|||
// 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) {
|
||||
func CookieAllowSubdomains(cookieNames ...string) CookieOption {
|
||||
return func(ctx Context, c *http.Cookie, _ uint8) {
|
||||
if c.Domain != "" {
|
||||
return // already set.
|
||||
}
|
||||
|
||||
if !findCookieAgainst(c, cookieNames) {
|
||||
if !CookieIncluded(c, cookieNames) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Domain = cookieDomain
|
||||
c.Domain = ctx.GetDomain()
|
||||
c.SameSite = http.SameSiteLaxMode // allow subdomain sharing.
|
||||
}
|
||||
}
|
||||
|
@ -4846,7 +4880,7 @@ func CookieAllowSubdomains(ctx Context, cookieNames ...string) CookieOption {
|
|||
//
|
||||
// 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) {
|
||||
return func(_ Context, c *http.Cookie, op uint8) {
|
||||
if op == OpCookieSet {
|
||||
c.SameSite = sameSite
|
||||
}
|
||||
|
@ -4855,12 +4889,10 @@ func CookieSameSite(sameSite http.SameSite) CookieOption {
|
|||
|
||||
// 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
|
||||
}
|
||||
func CookieSecure(ctx Context, c *http.Cookie, op uint8) {
|
||||
if op == OpCookieSet {
|
||||
if ctx.Request().TLS != nil {
|
||||
c.Secure = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4870,7 +4902,7 @@ func CookieSecure(ctx Context) CookieOption {
|
|||
// HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`.
|
||||
// See `CookieSecure` too.
|
||||
func CookieHTTPOnly(httpOnly bool) CookieOption {
|
||||
return func(c *http.Cookie, op uint8) {
|
||||
return func(_ Context, c *http.Cookie, op uint8) {
|
||||
if op == OpCookieSet {
|
||||
c.HttpOnly = httpOnly
|
||||
}
|
||||
|
@ -4880,7 +4912,7 @@ func CookieHTTPOnly(httpOnly bool) CookieOption {
|
|||
// 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) {
|
||||
return func(_ Context, c *http.Cookie, op uint8) {
|
||||
if op > OpCookieGet { // on set and remove.
|
||||
c.Path = path
|
||||
}
|
||||
|
@ -4889,7 +4921,7 @@ func CookiePath(path string) CookieOption {
|
|||
|
||||
// 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) {
|
||||
func CookieCleanPath(_ Context, c *http.Cookie, op uint8) {
|
||||
if op > OpCookieGet {
|
||||
c.Path = ""
|
||||
}
|
||||
|
@ -4898,7 +4930,7 @@ func CookieCleanPath(c *http.Cookie, op uint8) {
|
|||
// 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) {
|
||||
return func(_ Context, c *http.Cookie, op uint8) {
|
||||
if op == OpCookieSet {
|
||||
c.Expires = time.Now().Add(durFromNow)
|
||||
c.MaxAge = int(durFromNow.Seconds())
|
||||
|
@ -4945,12 +4977,12 @@ type SecureCookie interface {
|
|||
//
|
||||
// Example: https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie
|
||||
func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption {
|
||||
return func(c *http.Cookie, op uint8) {
|
||||
return func(_ Context, c *http.Cookie, op uint8) {
|
||||
if op == OpCookieDel {
|
||||
return
|
||||
}
|
||||
|
||||
if !findCookieAgainst(c, cookieNames) {
|
||||
if !CookieIncluded(c, cookieNames) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -5010,7 +5042,7 @@ func (ctx *context) applyCookieOptions(c *http.Cookie, op uint8, override []Cook
|
|||
if v := ctx.Values().Get(cookieOptionsContextKey); v != nil {
|
||||
if options, ok := v.([]CookieOption); ok {
|
||||
for _, opt := range options {
|
||||
opt(c, op)
|
||||
opt(ctx, c, op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5018,7 +5050,7 @@ func (ctx *context) applyCookieOptions(c *http.Cookie, op uint8, override []Cook
|
|||
// 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)
|
||||
opt(ctx, c, op)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -104,20 +104,21 @@ const sessionContextKey = "iris.session"
|
|||
func (s *Sessions) Handler(cookieOptions ...context.CookieOption) context.Handler {
|
||||
s.handlerCookieOpts = cookieOptions
|
||||
|
||||
var requestOptions []context.CookieOption
|
||||
if s.config.AllowReclaim {
|
||||
requestOptions = append(requestOptions, context.CookieAllowReclaim(s.config.Cookie))
|
||||
}
|
||||
if !s.config.DisableSubdomainPersistence {
|
||||
requestOptions = append(requestOptions, context.CookieAllowSubdomains(s.config.Cookie))
|
||||
}
|
||||
if s.config.CookieSecureTLS {
|
||||
requestOptions = append(requestOptions, context.CookieSecure)
|
||||
}
|
||||
if s.config.Encoding != nil {
|
||||
requestOptions = append(requestOptions, context.CookieEncoding(s.config.Encoding, s.config.Cookie))
|
||||
}
|
||||
|
||||
return func(ctx context.Context) {
|
||||
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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user