mirror of
https://github.com/wahyd4/loginsrv.git
synced 2026-08-09 04:46:29 +10:00
Added -cookie-expiry, -cookie-domain, -jwt-expiry parameters
This commit is contained in:
@@ -38,23 +38,29 @@ For questions and support please use the [Gitter chat room](https://gitter.im/ta
|
||||
|
||||
## Configuration and Startup
|
||||
### Config Options
|
||||
| Parameter | Type | Description | Default |
|
||||
|-|-|-|-|
|
||||
| -cookie-http-only | boolean | Set the cookie with the http only flag | true |
|
||||
| -cookie-name | string | The name of the jwt cookie | "jwt_token" |
|
||||
| -github | value | Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] | |
|
||||
| -host | string | The host to listen on | "localhost" |
|
||||
| -htpasswd | value | Htpasswd login backend opts: file=/path/to/pwdfile | |
|
||||
| -jwt-secret | string | The secret to sign the jwt token | "random key" |
|
||||
| -log-level | string | The log level | "info" |
|
||||
| -login-path | string | The path of the login resource | "/login" |
|
||||
| -logout-url | string | The url or path to redirect after logout | |
|
||||
| -osiam | value | Osiam login backend opts: endpoint=..,client_id=..,client_secret=.. | |
|
||||
| -port | string | The port to listen on | "6789" |
|
||||
| -simple | value | Simple login backend opts: user1=password,user2=password,.. | |
|
||||
| -success-url | string | The url to redirect after login | "/" |
|
||||
| -template | string | An alternative template for the login form | |
|
||||
| -text-logging | boolean | Log in text format instead of json | true |
|
||||
|
||||
_Note for caddy users_: Not all parameters are available in caddy. See the table for details. Incaddy, the parameter names can be also be used with `_` in the names, e.g. `cookie_http_only`.
|
||||
|
||||
| Parameter | Type | Default | Caddy | Description |
|
||||
|-------------------|-------------|--------------|-------|--------------------------------------------------------------------------------------|
|
||||
| -cookie-domain | string | | X | The optional domain parameter for the cookie |
|
||||
| -cookie-expiry | string | session | X | The expiry duration for the cookie, e.g. 2h or 3h30m |
|
||||
| -cookie-http-only | boolean | true | X | Set the cookie with the http only flag |
|
||||
| -cookie-name | string | "jwt_token" | X | The name of the jwt cookie |
|
||||
| -github | value | | X | Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] |
|
||||
| -host | string | "localhost" | - | The host to listen on |
|
||||
| -htpasswd | value | | X | Htpasswd login backend opts: file=/path/to/pwdfile |
|
||||
| -jwt-expiry | go duration | 24h | X | The expiry duration for the jwt token, e.g. 2h or 3h30m |
|
||||
| -jwt-secret | string | "random key" | X | The secret to sign the jwt token |
|
||||
| -log-level | string | "info" | - | The log level |
|
||||
| -login-path | string | "/login" | X | The path of the login resource |
|
||||
| -logout-url | string | | X | The url or path to redirect after logout |
|
||||
| -osiam | value | | X | OSIAM login backend opts: endpoint=..,client_id=..,client_secret=.. |
|
||||
| -port | string | "6789" | - | The port to listen on |
|
||||
| -simple | value | | X | Simple login backend opts: user1=password,user2=password,.. |
|
||||
| -success-url | string | "/" | X | The url to redirect after login |
|
||||
| -template | string | | X | An alternative template for the login form |
|
||||
| -text-logging | boolean | true | - | Log in text format instead of json |
|
||||
|
||||
### Environment Variables
|
||||
All of the above Config Options can also be applied as environment variable, where the name is written in the way: `LOGINSRV_OPTION_NAME`.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -26,6 +27,7 @@ func TestSetup(t *testing.T) {
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessUrl: "/",
|
||||
LoginPath: "/login",
|
||||
CookieName: "jwt_token",
|
||||
@@ -40,18 +42,24 @@ func TestSetup(t *testing.T) {
|
||||
{
|
||||
input: `loginsrv {
|
||||
success_url successurl
|
||||
jwt_expiry 42h
|
||||
login_path /foo/bar
|
||||
cookie_name cookiename
|
||||
cookie_http_only false
|
||||
cookie_domain example.com
|
||||
cookie_expiry 23h23m
|
||||
simple bob=secret
|
||||
osiam endpoint=http://localhost:8080,client_id=example-client,client_secret=secret
|
||||
}`,
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42 * time.Hour,
|
||||
SuccessUrl: "successurl",
|
||||
LoginPath: "/foo/bar",
|
||||
CookieName: "cookiename",
|
||||
CookieDomain: "example.com",
|
||||
CookieExpiry: 23*time.Hour + 23*time.Minute,
|
||||
CookieHttpOnly: false,
|
||||
Backends: login.Options{
|
||||
"simple": map[string]string{
|
||||
@@ -76,6 +84,7 @@ func TestSetup(t *testing.T) {
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessUrl: "/",
|
||||
LoginPath: "/context/login",
|
||||
CookieName: "cookiename",
|
||||
@@ -98,6 +107,7 @@ func TestSetup(t *testing.T) {
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessUrl: "/",
|
||||
LoginPath: "/login",
|
||||
CookieName: "cookiename",
|
||||
|
||||
@@ -25,6 +25,7 @@ func DefaultConfig() *Config {
|
||||
Port: "6789",
|
||||
LogLevel: "info",
|
||||
JwtSecret: jwtDefaultSecret,
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessUrl: "/",
|
||||
LogoutUrl: "",
|
||||
LoginPath: "/login",
|
||||
@@ -43,11 +44,14 @@ type Config struct {
|
||||
LogLevel string
|
||||
TextLogging bool
|
||||
JwtSecret string
|
||||
JwtExpiry time.Duration
|
||||
SuccessUrl string
|
||||
LogoutUrl string
|
||||
Template string
|
||||
LoginPath string
|
||||
CookieName string
|
||||
CookieExpiry time.Duration
|
||||
CookieDomain string
|
||||
CookieHttpOnly bool
|
||||
Backends Options
|
||||
Oauth Options
|
||||
@@ -86,8 +90,11 @@ func (c *Config) ConfigureFlagSet(f *flag.FlagSet) {
|
||||
f.StringVar(&c.LogLevel, "log-level", c.LogLevel, "The log level")
|
||||
f.BoolVar(&c.TextLogging, "text-logging", c.TextLogging, "Log in text format instead of json")
|
||||
f.StringVar(&c.JwtSecret, "jwt-secret", "random key", "The secret to sign the jwt token")
|
||||
f.DurationVar(&c.JwtExpiry, "jwt-expiry", c.JwtExpiry, "The expiry duration for the jwt token, e.g. 2h or 3h30m")
|
||||
f.StringVar(&c.CookieName, "cookie-name", c.CookieName, "The name of the jwt cookie")
|
||||
f.BoolVar(&c.CookieHttpOnly, "cookie-http-only", c.CookieHttpOnly, "Set the cookie with the http only flag")
|
||||
f.DurationVar(&c.CookieExpiry, "cookie-expiry", c.CookieExpiry, "The expiry duration for the cookie, e.g. 2h or 3h30m. Default is browser session")
|
||||
f.StringVar(&c.CookieDomain, "cookie-domain", c.CookieDomain, "The optional domain parameter for the cookie")
|
||||
f.StringVar(&c.SuccessUrl, "success-url", c.SuccessUrl, "The url to redirect after login")
|
||||
f.StringVar(&c.LogoutUrl, "logout-url", c.LogoutUrl, "The url or path to redirect after logout")
|
||||
f.StringVar(&c.Template, "template", c.Template, "An alternative template for the login form")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfig_ReadConfigDefaults(t *testing.T) {
|
||||
@@ -25,11 +26,14 @@ func TestConfig_ReadConfig(t *testing.T) {
|
||||
"--log-level=loglevel",
|
||||
"--text-logging=true",
|
||||
"--jwt-secret=jwtsecret",
|
||||
"--jwt-expiry=42h42m",
|
||||
"--success-url=successurl",
|
||||
"--logout-url=logouturl",
|
||||
"--template=template",
|
||||
"--login-path=loginpath",
|
||||
"--cookie-name=cookiename",
|
||||
"--cookie-expiry=23m",
|
||||
"--cookie-domain=*.example.com",
|
||||
"--cookie-http-only=false",
|
||||
"--backend=provider=simple",
|
||||
"--backend=provider=foo",
|
||||
@@ -42,11 +46,14 @@ func TestConfig_ReadConfig(t *testing.T) {
|
||||
LogLevel: "loglevel",
|
||||
TextLogging: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42*time.Hour + 42*time.Minute,
|
||||
SuccessUrl: "successurl",
|
||||
LogoutUrl: "logouturl",
|
||||
Template: "template",
|
||||
LoginPath: "loginpath",
|
||||
CookieName: "cookiename",
|
||||
CookieExpiry: 23 * time.Minute,
|
||||
CookieDomain: "*.example.com",
|
||||
CookieHttpOnly: false,
|
||||
Backends: Options{
|
||||
"simple": map[string]string{},
|
||||
@@ -71,11 +78,14 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_LOG_LEVEL", "loglevel"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_TEXT_LOGGING", "true"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_JWT_SECRET", "jwtsecret"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_JWT_EXPIRY", "42h42m"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_SUCCESS_URL", "successurl"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_LOGOUT_URL", "logouturl"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_TEMPLATE", "template"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_LOGIN_PATH", "loginpath"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_NAME", "cookiename"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_EXPIRY", "23m"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_DOMAIN", "*.example.com"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_HTTP_ONLY", "false"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_SIMPLE", "foo=bar"))
|
||||
assert.NoError(t, os.Setenv("LOGINSRV_GITHUB", "client_id=foo,client_secret=bar"))
|
||||
@@ -86,11 +96,14 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
|
||||
LogLevel: "loglevel",
|
||||
TextLogging: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42*time.Hour + 42*time.Minute,
|
||||
SuccessUrl: "successurl",
|
||||
LogoutUrl: "logouturl",
|
||||
Template: "template",
|
||||
LoginPath: "loginpath",
|
||||
CookieName: "cookiename",
|
||||
CookieExpiry: 23 * time.Minute,
|
||||
CookieDomain: "*.example.com",
|
||||
CookieHttpOnly: false,
|
||||
Backends: Options{
|
||||
"simple": map[string]string{
|
||||
|
||||
+10
-1
@@ -173,11 +173,14 @@ func (h *Handler) deleteToken(w http.ResponseWriter) {
|
||||
Expires: time.Unix(0, 0),
|
||||
Path: "/",
|
||||
}
|
||||
if h.config.CookieDomain != "" {
|
||||
cookie.Domain = h.config.CookieDomain
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, userInfo model.UserInfo) {
|
||||
userInfo.Expiry = time.Now().Add(time.Hour * 24).Unix()
|
||||
userInfo.Expiry = time.Now().Add(h.config.JwtExpiry).Unix()
|
||||
token, err := h.createToken(userInfo)
|
||||
if err != nil {
|
||||
logging.Application(r.Header).WithError(err).Error()
|
||||
@@ -191,6 +194,12 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u
|
||||
HttpOnly: h.config.CookieHttpOnly,
|
||||
Path: "/",
|
||||
}
|
||||
if h.config.CookieExpiry != 0 {
|
||||
cookie.Expires = time.Now().Add(h.config.CookieExpiry)
|
||||
}
|
||||
if h.config.CookieDomain != "" {
|
||||
cookie.Domain = h.config.CookieDomain
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
w.Header().Set("Location", h.config.SuccessUrl)
|
||||
w.WriteHeader(303)
|
||||
|
||||
+125
-18
@@ -9,8 +9,10 @@ import (
|
||||
"github.com/tarent/loginsrv/oauth2"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const TypeJson = "Content-Type: application/json"
|
||||
@@ -18,6 +20,14 @@ const TypeForm = "Content-Type: application/x-www-form-urlencoded"
|
||||
const AcceptHtml = "Accept: text/html"
|
||||
const AcceptJwt = "Accept: application/jwt"
|
||||
|
||||
func testConfig() *Config {
|
||||
testConfig := DefaultConfig()
|
||||
testConfig.LoginPath = "/context/login"
|
||||
testConfig.CookieDomain = "example.com"
|
||||
testConfig.CookieExpiry = 23 * time.Hour
|
||||
return testConfig
|
||||
}
|
||||
|
||||
func TestHandler_NewFromConfig(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
@@ -121,7 +131,8 @@ func TestHandler_LoginJson(t *testing.T) {
|
||||
// verify the token
|
||||
claims, err := tokenAsMap(recorder.Body.String())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]interface{}{"sub": "bob"}, claims)
|
||||
assert.Equal(t, "bob", claims["sub"])
|
||||
assert.InDelta(t, time.Now().Add(DefaultConfig().JwtExpiry).Unix(), claims["exp"], 2)
|
||||
|
||||
// wrong credentials
|
||||
recorder = call(req("POST", "/context/login", `{"username": "bob", "password": "FOOOBAR"}`, TypeJson, AcceptJwt))
|
||||
@@ -203,14 +214,21 @@ func TestHandler_LoginWeb(t *testing.T) {
|
||||
assert.Equal(t, "/", recorder.Header().Get("Location"))
|
||||
|
||||
// verify the token from the cookie
|
||||
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=")
|
||||
headerParts := strings.SplitN(recorder.Header().Get("Set-Cookie"), "=", 2)
|
||||
assert.Equal(t, 2, len(headerParts))
|
||||
assert.Equal(t, headerParts[0], "jwt_token")
|
||||
claims, err := tokenAsMap(strings.SplitN(headerParts[1], ";", 2)[0])
|
||||
setCookieList := readSetCookies(recorder.Header())
|
||||
assert.Equal(t, 1, len(setCookieList))
|
||||
|
||||
cookie := setCookieList[0]
|
||||
assert.Equal(t, "jwt_token", cookie.Name)
|
||||
assert.Equal(t, "/", cookie.Path)
|
||||
assert.Equal(t, "example.com", cookie.Domain)
|
||||
assert.InDelta(t, time.Now().Add(testConfig().CookieExpiry).Unix(), cookie.Expires.Unix(), 2)
|
||||
assert.True(t, cookie.HttpOnly)
|
||||
|
||||
// check the token contens
|
||||
claims, err := tokenAsMap(cookie.Value)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]interface{}{"sub": "bob"}, claims)
|
||||
assert.Contains(t, headerParts[1]+";", "Path=/;")
|
||||
assert.Equal(t, "bob", claims["sub"])
|
||||
assert.InDelta(t, time.Now().Add(DefaultConfig().JwtExpiry).Unix(), claims["exp"], 2)
|
||||
|
||||
// show the login form again after authentication failed
|
||||
recorder = call(req("POST", "/context/login", "username=bob&password=FOOBAR", TypeForm, AcceptHtml))
|
||||
@@ -223,21 +241,32 @@ func TestHandler_Logout(t *testing.T) {
|
||||
// DELETE
|
||||
recorder := call(req("DELETE", "/context/login", ""))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
|
||||
checkDeleteCookei(t, recorder.Header())
|
||||
|
||||
// GET + param
|
||||
recorder = call(req("GET", "/context/login?logout=true", ""))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
|
||||
checkDeleteCookei(t, recorder.Header())
|
||||
|
||||
// POST + param
|
||||
recorder = call(req("POST", "/context/login", "logout=true", TypeForm))
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
|
||||
checkDeleteCookei(t, recorder.Header())
|
||||
|
||||
assert.Equal(t, "no-cache, no-store, must-revalidate", recorder.Header().Get("Cache-Control"))
|
||||
}
|
||||
|
||||
func checkDeleteCookei(t *testing.T, h http.Header) {
|
||||
setCookieList := readSetCookies(h)
|
||||
assert.Equal(t, 1, len(setCookieList))
|
||||
cookie := setCookieList[0]
|
||||
|
||||
assert.Equal(t, "jwt_token", cookie.Name)
|
||||
assert.Equal(t, "/", cookie.Path)
|
||||
assert.Equal(t, "example.com", cookie.Domain)
|
||||
assert.Equal(t, int64(0), cookie.Expires.Unix())
|
||||
}
|
||||
|
||||
func TestHandler_CustomLogoutUrl(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.LogoutUrl = "http://example.com"
|
||||
@@ -322,26 +351,22 @@ func TestHandler_getToken_InvalidNoToken(t *testing.T) {
|
||||
}
|
||||
|
||||
func testHandler() *Handler {
|
||||
cfg := DefaultConfig()
|
||||
cfg.LoginPath = "/context/login"
|
||||
return &Handler{
|
||||
backends: []Backend{
|
||||
NewSimpleBackend(map[string]string{"bob": "secret"}),
|
||||
},
|
||||
oauth: oauth2.NewManager(),
|
||||
config: cfg,
|
||||
config: testConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func testHandlerWithError() *Handler {
|
||||
cfg := DefaultConfig()
|
||||
cfg.LoginPath = "/context/login"
|
||||
return &Handler{
|
||||
backends: []Backend{
|
||||
errorTestBackend("test error"),
|
||||
},
|
||||
oauth: oauth2.NewManager(),
|
||||
config: cfg,
|
||||
config: testConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +400,7 @@ func tokenAsMap(tokenString string) (map[string]interface{}, error) {
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return map[string]interface{}(claims), nil
|
||||
}
|
||||
|
||||
|
||||
return nil, errors.New("token not valid")
|
||||
}
|
||||
|
||||
@@ -408,3 +433,85 @@ func (m *oauth2ManagerMock) AddConfig(providerName string, opts map[string]strin
|
||||
func (m *oauth2ManagerMock) GetConfigFromRequest(r *http.Request) (oauth2.Config, error) {
|
||||
return m._GetConfigFromRequest(r)
|
||||
}
|
||||
|
||||
// copied from golang: net/http/cookie.go
|
||||
// with simple some simplification fro edge cases
|
||||
// readSetCookies parses all "Set-Cookie" values from
|
||||
// the header h and returns the successfully parsed Cookies.
|
||||
func readSetCookies(h http.Header) []*http.Cookie {
|
||||
cookieCount := len(h["Set-Cookie"])
|
||||
if cookieCount == 0 {
|
||||
return []*http.Cookie{}
|
||||
}
|
||||
cookies := make([]*http.Cookie, 0, cookieCount)
|
||||
for _, line := range h["Set-Cookie"] {
|
||||
parts := strings.Split(strings.TrimSpace(line), ";")
|
||||
if len(parts) == 1 && parts[0] == "" {
|
||||
continue
|
||||
}
|
||||
parts[0] = strings.TrimSpace(parts[0])
|
||||
j := strings.Index(parts[0], "=")
|
||||
if j < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
name, value := parts[0][:j], parts[0][j+1:]
|
||||
|
||||
c := &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Raw: line,
|
||||
}
|
||||
|
||||
for i := 1; i < len(parts); i++ {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
if len(parts[i]) == 0 {
|
||||
continue
|
||||
}
|
||||
attr, val := parts[i], ""
|
||||
if j := strings.Index(attr, "="); j >= 0 {
|
||||
attr, val = attr[:j], attr[j+1:]
|
||||
}
|
||||
lowerAttr := strings.ToLower(attr)
|
||||
switch lowerAttr {
|
||||
case "secure":
|
||||
c.Secure = true
|
||||
continue
|
||||
case "httponly":
|
||||
c.HttpOnly = true
|
||||
continue
|
||||
case "domain":
|
||||
c.Domain = val
|
||||
continue
|
||||
case "max-age":
|
||||
secs, err := strconv.Atoi(val)
|
||||
if err != nil || secs != 0 && val[0] == '0' {
|
||||
break
|
||||
}
|
||||
if secs <= 0 {
|
||||
secs = -1
|
||||
}
|
||||
c.MaxAge = secs
|
||||
continue
|
||||
case "expires":
|
||||
c.RawExpires = val
|
||||
exptime, err := time.Parse(time.RFC1123, val)
|
||||
if err != nil {
|
||||
exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
|
||||
if err != nil {
|
||||
c.Expires = time.Time{}
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Expires = exptime.UTC()
|
||||
continue
|
||||
case "path":
|
||||
c.Path = val
|
||||
continue
|
||||
}
|
||||
c.Unparsed = append(c.Unparsed, parts[i])
|
||||
}
|
||||
cookies = append(cookies, c)
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user