Merge pull request #1549 from FlyingBlazer/cookie-secure-config

ghttp: add cookie security configurations
This commit is contained in:
John Guo
2022-03-04 17:42:44 +08:00
committed by GitHub
5 changed files with 134 additions and 6 deletions

View File

@ -150,6 +150,18 @@ type ServerConfig struct {
// It also affects the default storage for session id.
CookieDomain string `json:"cookieDomain"`
// CookieSameSite specifies cookie SameSite property.
// It also affects the default storage for session id.
CookieSameSite string `json:"cookieSameSite"`
// CookieSameSite specifies cookie Secure property.
// It also affects the default storage for session id.
CookieSecure bool `json:"cookieSecure"`
// CookieSameSite specifies cookie HttpOnly property.
// It also affects the default storage for session id.
CookieHttpOnly bool `json:"cookieHttpOnly"`
// ======================================================================================================
// Session.
// ======================================================================================================

View File

@ -7,6 +7,7 @@
package ghttp
import (
"net/http"
"time"
)
@ -39,3 +40,25 @@ func (s *Server) GetCookiePath() string {
func (s *Server) GetCookieDomain() string {
return s.config.CookieDomain
}
// GetCookieSameSite return CookieSameSite of server.
func (s *Server) GetCookieSameSite() http.SameSite {
switch s.config.CookieSameSite {
case "lax":
return http.SameSiteLaxMode
case "none":
return http.SameSiteNoneMode
case "strict":
return http.SameSiteStrictMode
default:
return http.SameSiteDefaultMode
}
}
func (s *Server) GetCookieSecure() bool {
return s.config.CookieSecure
}
func (s *Server) GetCookieHttpOnly() bool {
return s.config.CookieHttpOnly
}

View File

@ -21,6 +21,13 @@ type Cookie struct {
response *Response // Belonged HTTP response.
}
// CookieOptions provides security config for cookies
type CookieOptions struct {
SameSite http.SameSite // cookie SameSite property
Secure bool // cookie Secure property
HttpOnly bool // cookie HttpOnly property
}
// cookieItem is the item stored in Cookie.
type cookieItem struct {
*http.Cookie // Underlying cookie items.
@ -88,24 +95,31 @@ func (c *Cookie) Set(key, value string) {
c.request.Server.GetCookieDomain(),
c.request.Server.GetCookiePath(),
c.request.Server.GetCookieMaxAge(),
CookieOptions{
SameSite: c.request.Server.GetCookieSameSite(),
Secure: c.request.Server.GetCookieSecure(),
HttpOnly: c.request.Server.GetCookieHttpOnly(),
},
)
}
// SetCookie sets cookie item with given domain, path and expiration age.
// The optional parameter `httpOnly` specifies if the cookie item is only available in HTTP,
// The optional parameter `options` specifies extra security configurations,
// which is usually empty.
func (c *Cookie) SetCookie(key, value, domain, path string, maxAge time.Duration, httpOnly ...bool) {
func (c *Cookie) SetCookie(key, value, domain, path string, maxAge time.Duration, options ...CookieOptions) {
c.init()
isHttpOnly := false
if len(httpOnly) > 0 {
isHttpOnly = httpOnly[0]
config := CookieOptions{}
if len(options) > 0 {
config = options[0]
}
httpCookie := &http.Cookie{
Name: key,
Value: value,
Path: path,
Domain: domain,
HttpOnly: isHttpOnly,
HttpOnly: config.HttpOnly,
SameSite: config.SameSite,
Secure: config.Secure,
}
if maxAge != 0 {
httpCookie.Expires = time.Now().Add(maxAge)
@ -136,6 +150,11 @@ func (c *Cookie) SetSessionId(id string) {
c.request.Server.GetCookieDomain(),
c.request.Server.GetCookiePath(),
c.server.GetSessionCookieMaxAge(),
CookieOptions{
SameSite: c.request.Server.GetCookieSameSite(),
Secure: c.request.Server.GetCookieSecure(),
HttpOnly: c.request.Server.GetCookieHttpOnly(),
},
)
}

View File

@ -29,6 +29,9 @@ func Test_ConfigFromMap(t *testing.T) {
"indexFiles": g.Slice{"index.php", "main.php"},
"errorLogEnabled": true,
"cookieMaxAge": "1y",
"cookieSameSite": "lax",
"cookieSecure": true,
"cookieHttpOnly": true,
}
config, err := ghttp.ConfigFromMap(m)
t.Assert(err, nil)
@ -39,6 +42,9 @@ func Test_ConfigFromMap(t *testing.T) {
t.Assert(config.CookieMaxAge, d2)
t.Assert(config.IndexFiles, m["indexFiles"])
t.Assert(config.ErrorLogEnabled, m["errorLogEnabled"])
t.Assert(config.CookieSameSite, m["cookieSameSite"])
t.Assert(config.CookieSecure, m["cookieSecure"])
t.Assert(config.CookieHttpOnly, m["cookieHttpOnly"])
})
}
@ -55,6 +61,9 @@ func Test_SetConfigWithMap(t *testing.T) {
"SessionIdName": "MySessionId",
"SessionPath": "/tmp/MySessionStoragePath",
"SessionMaxAge": 24 * time.Hour,
"cookieSameSite": "lax",
"cookieSecure": true,
"cookieHttpOnly": true,
}
s := g.Server()
err := s.SetConfigWithMap(m)

View File

@ -9,6 +9,7 @@ package ghttp_test
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
@ -101,3 +102,67 @@ func Test_SetHttpCookie(t *testing.T) {
//t.Assert(client.GetContent(ctx, "/get?k=key2"), "200")
})
}
func Test_CookieOptionsDefault(t *testing.T) {
s := g.Server(guid.S())
s.BindHandler("/test", func(r *ghttp.Request) {
r.Cookie.Set(r.Get("k").String(), r.Get("v").String())
})
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetBrowserMode(true)
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()))
r1, e1 := client.Get(ctx, "/test?k=key1&v=100")
if r1 != nil {
defer r1.Close()
}
t.Assert(e1, nil)
t.Assert(r1.ReadAllString(), "")
parts := strings.Split(r1.Header.Get("Set-Cookie"), "; ")
t.AssertIN(len(parts), []int{3, 4}) // For go < 1.16 cookie always output "SameSite", see: https://github.com/golang/go/commit/542693e00529fbb4248fac614ece68b127a5ec4d
})
}
func Test_CookieOptions(t *testing.T) {
s := g.Server(guid.S())
s.SetConfigWithMap(g.Map{
"cookieSameSite": "lax",
"cookieSecure": true,
"cookieHttpOnly": true,
})
s.BindHandler("/test", func(r *ghttp.Request) {
r.Cookie.Set(r.Get("k").String(), r.Get("v").String())
})
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetBrowserMode(true)
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()))
r1, e1 := client.Get(ctx, "/test?k=key1&v=100")
if r1 != nil {
defer r1.Close()
}
t.Assert(e1, nil)
t.Assert(r1.ReadAllString(), "")
parts := strings.Split(r1.Header.Get("Set-Cookie"), "; ")
t.AssertEQ(len(parts), 6)
t.Assert(parts[3], "HttpOnly")
t.Assert(parts[4], "Secure")
t.Assert(parts[5], "SameSite=Lax")
})
}