diff --git a/net/ghttp/ghttp_server_config.go b/net/ghttp/ghttp_server_config.go index 099358983..fff0d387e 100644 --- a/net/ghttp/ghttp_server_config.go +++ b/net/ghttp/ghttp_server_config.go @@ -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. // ====================================================================================================== diff --git a/net/ghttp/ghttp_server_config_cookie.go b/net/ghttp/ghttp_server_config_cookie.go index 266462028..2282abf05 100644 --- a/net/ghttp/ghttp_server_config_cookie.go +++ b/net/ghttp/ghttp_server_config_cookie.go @@ -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 +} diff --git a/net/ghttp/ghttp_server_cookie.go b/net/ghttp/ghttp_server_cookie.go index 8275593d9..1e519d957 100644 --- a/net/ghttp/ghttp_server_cookie.go +++ b/net/ghttp/ghttp_server_cookie.go @@ -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(), + }, ) } diff --git a/net/ghttp/ghttp_z_unit_feature_config_test.go b/net/ghttp/ghttp_z_unit_feature_config_test.go index 0995f2abf..1aaca1f5f 100644 --- a/net/ghttp/ghttp_z_unit_feature_config_test.go +++ b/net/ghttp/ghttp_z_unit_feature_config_test.go @@ -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) diff --git a/net/ghttp/ghttp_z_unit_feature_cookie_test.go b/net/ghttp/ghttp_z_unit_feature_cookie_test.go index d190baa2a..b6aa8fa8c 100644 --- a/net/ghttp/ghttp_z_unit_feature_cookie_test.go +++ b/net/ghttp/ghttp_z_unit_feature_cookie_test.go @@ -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") + }) +}