Fix recovery middleware to render gitea style page. (#13857)
* Some changes to fix recovery * Move Recovery to middlewares * Remove trace code * Fix lint * add session middleware and remove dependent on macaron for sso * Fix panic 500 page rendering * Fix bugs * Fix fmt * Fix vendor * recover unnecessary change * Fix lint and addd some comments about the copied codes. * Use util.StatDir instead of com.StatDir Co-authored-by: 6543 <6543@obermui.de>tokarchuk/v1.17
parent
126c9331d6
commit
15a475b7db
@ -0,0 +1,33 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sso |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
) |
||||||
|
|
||||||
|
// SignedInUser returns the user object of signed user.
|
||||||
|
// It returns a bool value to indicate whether user uses basic auth or not.
|
||||||
|
func SignedInUser(req *http.Request, ds DataStore, sess SessionStore) (*models.User, bool) { |
||||||
|
if !models.HasEngine { |
||||||
|
return nil, false |
||||||
|
} |
||||||
|
|
||||||
|
// Try to sign in with each of the enabled plugins
|
||||||
|
for _, ssoMethod := range Methods() { |
||||||
|
if !ssoMethod.IsEnabled() { |
||||||
|
continue |
||||||
|
} |
||||||
|
user := ssoMethod.VerifyAuthData(req, ds, sess) |
||||||
|
if user != nil { |
||||||
|
_, isBasic := ssoMethod.(*Basic) |
||||||
|
return user, isBasic |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil, false |
||||||
|
} |
@ -1,41 +0,0 @@ |
|||||||
// Copyright 2013 Martini Authors
|
|
||||||
// Copyright 2014 The Macaron Authors
|
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
|
||||||
// not use this file except in compliance with the License. You may obtain
|
|
||||||
// a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
// License for the specific language governing permissions and limitations
|
|
||||||
// under the License.
|
|
||||||
|
|
||||||
package context |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log" |
|
||||||
|
|
||||||
"gitea.com/macaron/macaron" |
|
||||||
) |
|
||||||
|
|
||||||
// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
|
|
||||||
// Although similar to macaron.Recovery() the main difference is that this error will be created
|
|
||||||
// with the gitea 500 page.
|
|
||||||
func Recovery() macaron.Handler { |
|
||||||
return func(ctx *Context) { |
|
||||||
defer func() { |
|
||||||
if err := recover(); err != nil { |
|
||||||
combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) |
|
||||||
ctx.ServerError("PANIC:", combinedErr) |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
ctx.Next() |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,104 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package middlewares |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"time" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
) |
||||||
|
|
||||||
|
// NewCookie creates a cookie
|
||||||
|
func NewCookie(name, value string, maxAge int) *http.Cookie { |
||||||
|
return &http.Cookie{ |
||||||
|
Name: name, |
||||||
|
Value: value, |
||||||
|
HttpOnly: true, |
||||||
|
Path: setting.SessionConfig.CookiePath, |
||||||
|
Domain: setting.SessionConfig.Domain, |
||||||
|
MaxAge: maxAge, |
||||||
|
Secure: setting.SessionConfig.Secure, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SetCookie set the cookies
|
||||||
|
// TODO: Copied from gitea.com/macaron/macaron and should be improved after macaron removed.
|
||||||
|
func SetCookie(resp http.ResponseWriter, name string, value string, others ...interface{}) { |
||||||
|
cookie := http.Cookie{} |
||||||
|
cookie.Name = name |
||||||
|
cookie.Value = url.QueryEscape(value) |
||||||
|
|
||||||
|
if len(others) > 0 { |
||||||
|
switch v := others[0].(type) { |
||||||
|
case int: |
||||||
|
cookie.MaxAge = v |
||||||
|
case int64: |
||||||
|
cookie.MaxAge = int(v) |
||||||
|
case int32: |
||||||
|
cookie.MaxAge = int(v) |
||||||
|
case func(*http.Cookie): |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
cookie.Path = "/" |
||||||
|
if len(others) > 1 { |
||||||
|
if v, ok := others[1].(string); ok && len(v) > 0 { |
||||||
|
cookie.Path = v |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 2 { |
||||||
|
if v, ok := others[2].(string); ok && len(v) > 0 { |
||||||
|
cookie.Domain = v |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 3 { |
||||||
|
switch v := others[3].(type) { |
||||||
|
case bool: |
||||||
|
cookie.Secure = v |
||||||
|
case func(*http.Cookie): |
||||||
|
v(&cookie) |
||||||
|
default: |
||||||
|
if others[3] != nil { |
||||||
|
cookie.Secure = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 4 { |
||||||
|
if v, ok := others[4].(bool); ok && v { |
||||||
|
cookie.HttpOnly = true |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 5 { |
||||||
|
if v, ok := others[5].(time.Time); ok { |
||||||
|
cookie.Expires = v |
||||||
|
cookie.RawExpires = v.Format(time.UnixDate) |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 6 { |
||||||
|
for _, other := range others[6:] { |
||||||
|
if v, ok := other.(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
resp.Header().Add("Set-Cookie", cookie.String()) |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package middlewares |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/translation" |
||||||
|
|
||||||
|
"github.com/unknwon/i18n" |
||||||
|
"golang.org/x/text/language" |
||||||
|
) |
||||||
|
|
||||||
|
// Locale handle locale
|
||||||
|
func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale { |
||||||
|
hasCookie := false |
||||||
|
|
||||||
|
// 1. Check URL arguments.
|
||||||
|
lang := req.URL.Query().Get("lang") |
||||||
|
|
||||||
|
// 2. Get language information from cookies.
|
||||||
|
if len(lang) == 0 { |
||||||
|
ck, _ := req.Cookie("lang") |
||||||
|
lang = ck.Value |
||||||
|
hasCookie = true |
||||||
|
} |
||||||
|
|
||||||
|
// Check again in case someone modify by purpose.
|
||||||
|
if !i18n.IsExist(lang) { |
||||||
|
lang = "" |
||||||
|
hasCookie = false |
||||||
|
} |
||||||
|
|
||||||
|
// 3. Get language information from 'Accept-Language'.
|
||||||
|
// The first element in the list is chosen to be the default language automatically.
|
||||||
|
if len(lang) == 0 { |
||||||
|
tags, _, _ := language.ParseAcceptLanguage(req.Header.Get("Accept-Language")) |
||||||
|
tag, _, _ := translation.Match(tags...) |
||||||
|
lang = tag.String() |
||||||
|
} |
||||||
|
|
||||||
|
if !hasCookie { |
||||||
|
req.AddCookie(NewCookie("lang", lang, 1<<31-1)) |
||||||
|
} |
||||||
|
|
||||||
|
return translation.NewLocale(lang) |
||||||
|
} |
@ -0,0 +1,217 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package middlewares |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/nosql" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
"github.com/go-redis/redis/v7" |
||||||
|
) |
||||||
|
|
||||||
|
// RedisStore represents a redis session store implementation.
|
||||||
|
// TODO: copied from modules/session/redis.go and should remove that one until macaron removed.
|
||||||
|
type RedisStore struct { |
||||||
|
c redis.UniversalClient |
||||||
|
prefix, sid string |
||||||
|
duration time.Duration |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewRedisStore creates and returns a redis session store.
|
||||||
|
func NewRedisStore(c redis.UniversalClient, prefix, sid string, dur time.Duration, kv map[interface{}]interface{}) *RedisStore { |
||||||
|
return &RedisStore{ |
||||||
|
c: c, |
||||||
|
prefix: prefix, |
||||||
|
sid: sid, |
||||||
|
duration: dur, |
||||||
|
data: kv, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *RedisStore) Set(key, val interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = val |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *RedisStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete delete a key from session.
|
||||||
|
func (s *RedisStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *RedisStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases resource and save data to provider.
|
||||||
|
func (s *RedisStore) Release() error { |
||||||
|
// Skip encoding if the data is empty
|
||||||
|
if len(s.data) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
data, err := session.EncodeGob(s.data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return s.c.Set(s.prefix+s.sid, string(data), s.duration).Err() |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *RedisStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// RedisProvider represents a redis session provider implementation.
|
||||||
|
type RedisProvider struct { |
||||||
|
c redis.UniversalClient |
||||||
|
duration time.Duration |
||||||
|
prefix string |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes redis session provider.
|
||||||
|
// configs: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180,prefix=session;
|
||||||
|
func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) { |
||||||
|
p.duration, err = time.ParseDuration(fmt.Sprintf("%ds", maxlifetime)) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
uri := nosql.ToRedisURI(configs) |
||||||
|
|
||||||
|
for k, v := range uri.Query() { |
||||||
|
switch k { |
||||||
|
case "prefix": |
||||||
|
p.prefix = v[0] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
p.c = nosql.GetManager().GetRedisClient(uri.String()) |
||||||
|
return p.c.Ping().Err() |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (p *RedisProvider) Read(sid string) (session.RawStore, error) { |
||||||
|
psid := p.prefix + sid |
||||||
|
if !p.Exist(sid) { |
||||||
|
if err := p.c.Set(psid, "", p.duration).Err(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var kv map[interface{}]interface{} |
||||||
|
kvs, err := p.c.Get(psid).Result() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(kvs) == 0 { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob([]byte(kvs)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NewRedisStore(p.c, p.prefix, sid, p.duration, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (p *RedisProvider) Exist(sid string) bool { |
||||||
|
v, err := p.c.Exists(p.prefix + sid).Result() |
||||||
|
return err == nil && v == 1 |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (p *RedisProvider) Destroy(sid string) error { |
||||||
|
return p.c.Del(p.prefix + sid).Err() |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { |
||||||
|
poldsid := p.prefix + oldsid |
||||||
|
psid := p.prefix + sid |
||||||
|
|
||||||
|
if p.Exist(sid) { |
||||||
|
return nil, fmt.Errorf("new sid '%s' already exists", sid) |
||||||
|
} else if !p.Exist(oldsid) { |
||||||
|
// Make a fake old session.
|
||||||
|
if err = p.c.Set(poldsid, "", p.duration).Err(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err = p.c.Rename(poldsid, psid).Err(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var kv map[interface{}]interface{} |
||||||
|
kvs, err := p.c.Get(psid).Result() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if len(kvs) == 0 { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob([]byte(kvs)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NewRedisStore(p.c, p.prefix, sid, p.duration, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (p *RedisProvider) Count() int { |
||||||
|
return int(p.c.DBSize().Val()) |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (*RedisProvider) GC() {} |
||||||
|
|
||||||
|
func init() { |
||||||
|
session.Register("redis", &RedisProvider{}) |
||||||
|
} |
@ -0,0 +1,196 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package middlewares |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
couchbase "gitea.com/go-chi/session/couchbase" |
||||||
|
memcache "gitea.com/go-chi/session/memcache" |
||||||
|
mysql "gitea.com/go-chi/session/mysql" |
||||||
|
postgres "gitea.com/go-chi/session/postgres" |
||||||
|
) |
||||||
|
|
||||||
|
// VirtualSessionProvider represents a shadowed session provider implementation.
|
||||||
|
// TODO: copied from modules/session/redis.go and should remove that one until macaron removed.
|
||||||
|
type VirtualSessionProvider struct { |
||||||
|
lock sync.RWMutex |
||||||
|
provider session.Provider |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes the cookie session provider with given root path.
|
||||||
|
func (o *VirtualSessionProvider) Init(gclifetime int64, config string) error { |
||||||
|
var opts session.Options |
||||||
|
if err := json.Unmarshal([]byte(config), &opts); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
// Note that these options are unprepared so we can't just use NewManager here.
|
||||||
|
// Nor can we access the provider map in session.
|
||||||
|
// So we will just have to do this by hand.
|
||||||
|
// This is only slightly more wrong than modules/setting/session.go:23
|
||||||
|
switch opts.Provider { |
||||||
|
case "memory": |
||||||
|
o.provider = &session.MemProvider{} |
||||||
|
case "file": |
||||||
|
o.provider = &session.FileProvider{} |
||||||
|
case "redis": |
||||||
|
o.provider = &RedisProvider{} |
||||||
|
case "mysql": |
||||||
|
o.provider = &mysql.MysqlProvider{} |
||||||
|
case "postgres": |
||||||
|
o.provider = &postgres.PostgresProvider{} |
||||||
|
case "couchbase": |
||||||
|
o.provider = &couchbase.CouchbaseProvider{} |
||||||
|
case "memcache": |
||||||
|
o.provider = &memcache.MemcacheProvider{} |
||||||
|
default: |
||||||
|
return fmt.Errorf("VirtualSessionProvider: Unknown Provider: %s", opts.Provider) |
||||||
|
} |
||||||
|
return o.provider.Init(gclifetime, opts.ProviderConfig) |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) { |
||||||
|
o.lock.RLock() |
||||||
|
defer o.lock.RUnlock() |
||||||
|
if o.provider.Exist(sid) { |
||||||
|
return o.provider.Read(sid) |
||||||
|
} |
||||||
|
kv := make(map[interface{}]interface{}) |
||||||
|
kv["_old_uid"] = "0" |
||||||
|
return NewVirtualStore(o, sid, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (o *VirtualSessionProvider) Exist(sid string) bool { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (o *VirtualSessionProvider) Destroy(sid string) error { |
||||||
|
o.lock.Lock() |
||||||
|
defer o.lock.Unlock() |
||||||
|
return o.provider.Destroy(sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (o *VirtualSessionProvider) Regenerate(oldsid, sid string) (session.RawStore, error) { |
||||||
|
o.lock.Lock() |
||||||
|
defer o.lock.Unlock() |
||||||
|
return o.provider.Regenerate(oldsid, sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (o *VirtualSessionProvider) Count() int { |
||||||
|
o.lock.RLock() |
||||||
|
defer o.lock.RUnlock() |
||||||
|
return o.provider.Count() |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (o *VirtualSessionProvider) GC() { |
||||||
|
o.provider.GC() |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
session.Register("VirtualSession", &VirtualSessionProvider{}) |
||||||
|
} |
||||||
|
|
||||||
|
// VirtualStore represents a virtual session store implementation.
|
||||||
|
type VirtualStore struct { |
||||||
|
p *VirtualSessionProvider |
||||||
|
sid string |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
released bool |
||||||
|
} |
||||||
|
|
||||||
|
// NewVirtualStore creates and returns a virtual session store.
|
||||||
|
func NewVirtualStore(p *VirtualSessionProvider, sid string, kv map[interface{}]interface{}) *VirtualStore { |
||||||
|
return &VirtualStore{ |
||||||
|
p: p, |
||||||
|
sid: sid, |
||||||
|
data: kv, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *VirtualStore) Set(key, val interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = val |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *VirtualStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete delete a key from session.
|
||||||
|
func (s *VirtualStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *VirtualStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases resource and save data to provider.
|
||||||
|
func (s *VirtualStore) Release() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
// Now need to lock the provider
|
||||||
|
s.p.lock.Lock() |
||||||
|
defer s.p.lock.Unlock() |
||||||
|
if oldUID, ok := s.data["_old_uid"]; (ok && (oldUID != "0" || len(s.data) > 1)) || (!ok && len(s.data) > 0) { |
||||||
|
// Now ensure that we don't exist!
|
||||||
|
realProvider := s.p.provider |
||||||
|
|
||||||
|
if !s.released && realProvider.Exist(s.sid) { |
||||||
|
// This is an error!
|
||||||
|
return fmt.Errorf("new sid '%s' already exists", s.sid) |
||||||
|
} |
||||||
|
realStore, err := realProvider.Read(s.sid) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := realStore.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for key, value := range s.data { |
||||||
|
if err := realStore.Set(key, value); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
err = realStore.Release() |
||||||
|
if err == nil { |
||||||
|
s.released = true |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *VirtualStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package templates |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
) |
||||||
|
|
||||||
|
// Vars represents variables to be render in golang templates
|
||||||
|
type Vars map[string]interface{} |
||||||
|
|
||||||
|
// Merge merges another vars to the current, another Vars will override the current
|
||||||
|
func (vars Vars) Merge(another map[string]interface{}) Vars { |
||||||
|
for k, v := range another { |
||||||
|
vars[k] = v |
||||||
|
} |
||||||
|
return vars |
||||||
|
} |
||||||
|
|
||||||
|
// BaseVars returns all basic vars
|
||||||
|
func BaseVars() Vars { |
||||||
|
var startTime = time.Now() |
||||||
|
return map[string]interface{}{ |
||||||
|
"IsLandingPageHome": setting.LandingPageURL == setting.LandingPageHome, |
||||||
|
"IsLandingPageExplore": setting.LandingPageURL == setting.LandingPageExplore, |
||||||
|
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, |
||||||
|
|
||||||
|
"ShowRegistrationButton": setting.Service.ShowRegistrationButton, |
||||||
|
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, |
||||||
|
"ShowFooterBranding": setting.ShowFooterBranding, |
||||||
|
"ShowFooterVersion": setting.ShowFooterVersion, |
||||||
|
|
||||||
|
"EnableSwagger": setting.API.EnableSwagger, |
||||||
|
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, |
||||||
|
"PageStartTime": startTime, |
||||||
|
"TmplLoadTimes": func() string { |
||||||
|
return time.Since(startTime).String() |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getDirAssetNames(dir string) []string { |
||||||
|
var tmpls []string |
||||||
|
f, err := os.Stat(dir) |
||||||
|
if err != nil { |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return tmpls |
||||||
|
} |
||||||
|
log.Warn("Unable to check if templates dir %s is a directory. Error: %v", dir, err) |
||||||
|
return tmpls |
||||||
|
} |
||||||
|
if !f.IsDir() { |
||||||
|
log.Warn("Templates dir %s is a not directory.", dir) |
||||||
|
return tmpls |
||||||
|
} |
||||||
|
|
||||||
|
files, err := util.StatDir(dir) |
||||||
|
if err != nil { |
||||||
|
log.Warn("Failed to read %s templates dir. %v", dir, err) |
||||||
|
return tmpls |
||||||
|
} |
||||||
|
for _, filePath := range files { |
||||||
|
if strings.HasPrefix(filePath, "mail/") { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if !strings.HasSuffix(filePath, ".tmpl") { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
tmpls = append(tmpls, "templates/"+filePath) |
||||||
|
} |
||||||
|
return tmpls |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package translation |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/options" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
|
||||||
|
macaron_i18n "gitea.com/macaron/i18n" |
||||||
|
"github.com/unknwon/i18n" |
||||||
|
"golang.org/x/text/language" |
||||||
|
) |
||||||
|
|
||||||
|
// Locale represents an interface to translation
|
||||||
|
type Locale interface { |
||||||
|
Language() string |
||||||
|
Tr(string, ...interface{}) string |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
matcher language.Matcher |
||||||
|
) |
||||||
|
|
||||||
|
// InitLocales loads the locales
|
||||||
|
func InitLocales() { |
||||||
|
localeNames, err := options.Dir("locale") |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
log.Fatal("Failed to list locale files: %v", err) |
||||||
|
} |
||||||
|
localFiles := make(map[string][]byte) |
||||||
|
|
||||||
|
for _, name := range localeNames { |
||||||
|
localFiles[name], err = options.Locale(name) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
log.Fatal("Failed to load %s locale file. %v", name, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// These codes will be used once macaron removed
|
||||||
|
/*tags := make([]language.Tag, len(setting.Langs)) |
||||||
|
for i, lang := range setting.Langs { |
||||||
|
tags[i] = language.Raw.Make(lang) |
||||||
|
} |
||||||
|
matcher = language.NewMatcher(tags) |
||||||
|
for i, name := range setting.Names { |
||||||
|
i18n.SetMessage(setting.Langs[i], localFiles[name]) |
||||||
|
} |
||||||
|
i18n.SetDefaultLang("en-US")*/ |
||||||
|
|
||||||
|
// To be compatible with macaron, we now have to use macaron i18n, once macaron
|
||||||
|
// removed, we can use i18n directly
|
||||||
|
macaron_i18n.I18n(macaron_i18n.Options{ |
||||||
|
SubURL: setting.AppSubURL, |
||||||
|
Files: localFiles, |
||||||
|
Langs: setting.Langs, |
||||||
|
Names: setting.Names, |
||||||
|
DefaultLang: "en-US", |
||||||
|
Redirect: false, |
||||||
|
CookieDomain: setting.SessionConfig.Domain, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Match matches accept languages
|
||||||
|
func Match(tags ...language.Tag) (tag language.Tag, index int, c language.Confidence) { |
||||||
|
return matcher.Match(tags...) |
||||||
|
} |
||||||
|
|
||||||
|
// locale represents the information of localization.
|
||||||
|
type locale struct { |
||||||
|
Lang string |
||||||
|
} |
||||||
|
|
||||||
|
// NewLocale return a locale
|
||||||
|
func NewLocale(lang string) Locale { |
||||||
|
return &locale{ |
||||||
|
Lang: lang, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (l *locale) Language() string { |
||||||
|
return l.Lang |
||||||
|
} |
||||||
|
|
||||||
|
// Tr translates content to target language.
|
||||||
|
func (l *locale) Tr(format string, args ...interface{}) string { |
||||||
|
return i18n.Tr(l.Lang, format, args...) |
||||||
|
} |
@ -0,0 +1,105 @@ |
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package routes |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/auth/sso" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"code.gitea.io/gitea/modules/middlewares" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/templates" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
"github.com/unrolled/render" |
||||||
|
) |
||||||
|
|
||||||
|
type dataStore struct { |
||||||
|
Data map[string]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *dataStore) GetData() map[string]interface{} { |
||||||
|
return d.Data |
||||||
|
} |
||||||
|
|
||||||
|
// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
|
||||||
|
// Although similar to macaron.Recovery() the main difference is that this error will be created
|
||||||
|
// with the gitea 500 page.
|
||||||
|
func Recovery() func(next http.Handler) http.Handler { |
||||||
|
var isDevelopment = setting.RunMode != "prod" |
||||||
|
return func(next http.Handler) http.Handler { |
||||||
|
rnd := render.New(render.Options{ |
||||||
|
Extensions: []string{".tmpl"}, |
||||||
|
Directory: "templates", |
||||||
|
Funcs: templates.NewFuncMap(), |
||||||
|
Asset: templates.GetAsset, |
||||||
|
AssetNames: templates.GetAssetNames, |
||||||
|
IsDevelopment: isDevelopment, |
||||||
|
}) |
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
||||||
|
defer func() { |
||||||
|
// Why we need this? The first recover will try to render a beautiful
|
||||||
|
// error page for user, but the process can still panic again, then
|
||||||
|
// we have to just recover twice and send a simple error page that
|
||||||
|
// should not panic any more.
|
||||||
|
defer func() { |
||||||
|
if err := recover(); err != nil { |
||||||
|
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2))) |
||||||
|
log.Error(combinedErr) |
||||||
|
if isDevelopment { |
||||||
|
http.Error(w, combinedErr, 500) |
||||||
|
} else { |
||||||
|
http.Error(w, http.StatusText(500), 500) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
if err := recover(); err != nil { |
||||||
|
combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2))) |
||||||
|
log.Error("%v", combinedErr) |
||||||
|
|
||||||
|
lc := middlewares.Locale(w, req) |
||||||
|
sess := session.GetSession(req) |
||||||
|
|
||||||
|
var store = dataStore{ |
||||||
|
Data: templates.Vars{ |
||||||
|
"Language": lc.Language(), |
||||||
|
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(), |
||||||
|
"i18n": lc, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
// Get user from session if logged in.
|
||||||
|
user, _ := sso.SignedInUser(req, &store, sess) |
||||||
|
if user != nil { |
||||||
|
store.Data["IsSigned"] = true |
||||||
|
store.Data["SignedUser"] = user |
||||||
|
store.Data["SignedUserID"] = user.ID |
||||||
|
store.Data["SignedUserName"] = user.Name |
||||||
|
store.Data["IsAdmin"] = user.IsAdmin |
||||||
|
} else { |
||||||
|
store.Data["SignedUserID"] = int64(0) |
||||||
|
store.Data["SignedUserName"] = "" |
||||||
|
} |
||||||
|
|
||||||
|
w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`) |
||||||
|
|
||||||
|
if setting.RunMode != "prod" { |
||||||
|
store.Data["ErrMsg"] = combinedErr |
||||||
|
} |
||||||
|
err := rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store.Data)) |
||||||
|
if err != nil { |
||||||
|
log.Error("%v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
next.ServeHTTP(w, req) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
--- |
||||||
|
kind: pipeline |
||||||
|
name: test |
||||||
|
|
||||||
|
platform: |
||||||
|
os: linux |
||||||
|
arch: amd64 |
||||||
|
|
||||||
|
clone: |
||||||
|
disable: true |
||||||
|
|
||||||
|
workspace: |
||||||
|
base: /go |
||||||
|
path: src/session113 |
||||||
|
|
||||||
|
steps: |
||||||
|
- name: git |
||||||
|
pull: default |
||||||
|
image: plugins/git:next |
||||||
|
settings: |
||||||
|
depth: 50 |
||||||
|
tags: true |
||||||
|
|
||||||
|
- name: test |
||||||
|
pull: default |
||||||
|
image: golang:1.13 |
||||||
|
environment: |
||||||
|
GO111MODULE: on |
||||||
|
GOPROXY: https://goproxy.cn |
||||||
|
commands: |
||||||
|
- go build -v |
||||||
|
- go vet ./... |
||||||
|
- go test -v -race -coverprofile=coverage.txt -covermode=atomic |
||||||
|
when: |
||||||
|
event: |
||||||
|
- push |
||||||
|
- pull_request |
@ -0,0 +1,4 @@ |
|||||||
|
ledis/tmp.db |
||||||
|
nodb/tmp.db |
||||||
|
/vendor |
||||||
|
/.idea |
@ -0,0 +1,191 @@ |
|||||||
|
Apache License |
||||||
|
Version 2.0, January 2004 |
||||||
|
http://www.apache.org/licenses/ |
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||||
|
|
||||||
|
1. Definitions. |
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, and |
||||||
|
distribution as defined by Sections 1 through 9 of this document. |
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright |
||||||
|
owner that is granting the License. |
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities |
||||||
|
that control, are controlled by, or are under common control with that entity. |
||||||
|
For the purposes of this definition, "control" means (i) the power, direct or |
||||||
|
indirect, to cause the direction or management of such entity, whether by |
||||||
|
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity. |
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising |
||||||
|
permissions granted by this License. |
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including |
||||||
|
but not limited to software source code, documentation source, and configuration |
||||||
|
files. |
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation or |
||||||
|
translation of a Source form, including but not limited to compiled object code, |
||||||
|
generated documentation, and conversions to other media types. |
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or Object form, made |
||||||
|
available under the License, as indicated by a copyright notice that is included |
||||||
|
in or attached to the work (an example is provided in the Appendix below). |
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object form, that |
||||||
|
is based on (or derived from) the Work and for which the editorial revisions, |
||||||
|
annotations, elaborations, or other modifications represent, as a whole, an |
||||||
|
original work of authorship. For the purposes of this License, Derivative Works |
||||||
|
shall not include works that remain separable from, or merely link (or bind by |
||||||
|
name) to the interfaces of, the Work and Derivative Works thereof. |
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including the original version |
||||||
|
of the Work and any modifications or additions to that Work or Derivative Works |
||||||
|
thereof, that is intentionally submitted to Licensor for inclusion in the Work |
||||||
|
by the copyright owner or by an individual or Legal Entity authorized to submit |
||||||
|
on behalf of the copyright owner. For the purposes of this definition, |
||||||
|
"submitted" means any form of electronic, verbal, or written communication sent |
||||||
|
to the Licensor or its representatives, including but not limited to |
||||||
|
communication on electronic mailing lists, source code control systems, and |
||||||
|
issue tracking systems that are managed by, or on behalf of, the Licensor for |
||||||
|
the purpose of discussing and improving the Work, but excluding communication |
||||||
|
that is conspicuously marked or otherwise designated in writing by the copyright |
||||||
|
owner as "Not a Contribution." |
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf |
||||||
|
of whom a Contribution has been received by Licensor and subsequently |
||||||
|
incorporated within the Work. |
||||||
|
|
||||||
|
2. Grant of Copyright License. |
||||||
|
|
||||||
|
Subject to the terms and conditions of this License, each Contributor hereby |
||||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, |
||||||
|
irrevocable copyright license to reproduce, prepare Derivative Works of, |
||||||
|
publicly display, publicly perform, sublicense, and distribute the Work and such |
||||||
|
Derivative Works in Source or Object form. |
||||||
|
|
||||||
|
3. Grant of Patent License. |
||||||
|
|
||||||
|
Subject to the terms and conditions of this License, each Contributor hereby |
||||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, |
||||||
|
irrevocable (except as stated in this section) patent license to make, have |
||||||
|
made, use, offer to sell, sell, import, and otherwise transfer the Work, where |
||||||
|
such license applies only to those patent claims licensable by such Contributor |
||||||
|
that are necessarily infringed by their Contribution(s) alone or by combination |
||||||
|
of their Contribution(s) with the Work to which such Contribution(s) was |
||||||
|
submitted. If You institute patent litigation against any entity (including a |
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work or a |
||||||
|
Contribution incorporated within the Work constitutes direct or contributory |
||||||
|
patent infringement, then any patent licenses granted to You under this License |
||||||
|
for that Work shall terminate as of the date such litigation is filed. |
||||||
|
|
||||||
|
4. Redistribution. |
||||||
|
|
||||||
|
You may reproduce and distribute copies of the Work or Derivative Works thereof |
||||||
|
in any medium, with or without modifications, and in Source or Object form, |
||||||
|
provided that You meet the following conditions: |
||||||
|
|
||||||
|
You must give any other recipients of the Work or Derivative Works a copy of |
||||||
|
this License; and |
||||||
|
You must cause any modified files to carry prominent notices stating that You |
||||||
|
changed the files; and |
||||||
|
You must retain, in the Source form of any Derivative Works that You distribute, |
||||||
|
all copyright, patent, trademark, and attribution notices from the Source form |
||||||
|
of the Work, excluding those notices that do not pertain to any part of the |
||||||
|
Derivative Works; and |
||||||
|
If the Work includes a "NOTICE" text file as part of its distribution, then any |
||||||
|
Derivative Works that You distribute must include a readable copy of the |
||||||
|
attribution notices contained within such NOTICE file, excluding those notices |
||||||
|
that do not pertain to any part of the Derivative Works, in at least one of the |
||||||
|
following places: within a NOTICE text file distributed as part of the |
||||||
|
Derivative Works; within the Source form or documentation, if provided along |
||||||
|
with the Derivative Works; or, within a display generated by the Derivative |
||||||
|
Works, if and wherever such third-party notices normally appear. The contents of |
||||||
|
the NOTICE file are for informational purposes only and do not modify the |
||||||
|
License. You may add Your own attribution notices within Derivative Works that |
||||||
|
You distribute, alongside or as an addendum to the NOTICE text from the Work, |
||||||
|
provided that such additional attribution notices cannot be construed as |
||||||
|
modifying the License. |
||||||
|
You may add Your own copyright statement to Your modifications and may provide |
||||||
|
additional or different license terms and conditions for use, reproduction, or |
||||||
|
distribution of Your modifications, or for any such Derivative Works as a whole, |
||||||
|
provided Your use, reproduction, and distribution of the Work otherwise complies |
||||||
|
with the conditions stated in this License. |
||||||
|
|
||||||
|
5. Submission of Contributions. |
||||||
|
|
||||||
|
Unless You explicitly state otherwise, any Contribution intentionally submitted |
||||||
|
for inclusion in the Work by You to the Licensor shall be under the terms and |
||||||
|
conditions of this License, without any additional terms or conditions. |
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify the terms of |
||||||
|
any separate license agreement you may have executed with Licensor regarding |
||||||
|
such Contributions. |
||||||
|
|
||||||
|
6. Trademarks. |
||||||
|
|
||||||
|
This License does not grant permission to use the trade names, trademarks, |
||||||
|
service marks, or product names of the Licensor, except as required for |
||||||
|
reasonable and customary use in describing the origin of the Work and |
||||||
|
reproducing the content of the NOTICE file. |
||||||
|
|
||||||
|
7. Disclaimer of Warranty. |
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, Licensor provides the |
||||||
|
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, |
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, |
||||||
|
including, without limitation, any warranties or conditions of TITLE, |
||||||
|
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are |
||||||
|
solely responsible for determining the appropriateness of using or |
||||||
|
redistributing the Work and assume any risks associated with Your exercise of |
||||||
|
permissions under this License. |
||||||
|
|
||||||
|
8. Limitation of Liability. |
||||||
|
|
||||||
|
In no event and under no legal theory, whether in tort (including negligence), |
||||||
|
contract, or otherwise, unless required by applicable law (such as deliberate |
||||||
|
and grossly negligent acts) or agreed to in writing, shall any Contributor be |
||||||
|
liable to You for damages, including any direct, indirect, special, incidental, |
||||||
|
or consequential damages of any character arising as a result of this License or |
||||||
|
out of the use or inability to use the Work (including but not limited to |
||||||
|
damages for loss of goodwill, work stoppage, computer failure or malfunction, or |
||||||
|
any and all other commercial damages or losses), even if such Contributor has |
||||||
|
been advised of the possibility of such damages. |
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. |
||||||
|
|
||||||
|
While redistributing the Work or Derivative Works thereof, You may choose to |
||||||
|
offer, and charge a fee for, acceptance of support, warranty, indemnity, or |
||||||
|
other liability obligations and/or rights consistent with this License. However, |
||||||
|
in accepting such obligations, You may act only on Your own behalf and on Your |
||||||
|
sole responsibility, not on behalf of any other Contributor, and only if You |
||||||
|
agree to indemnify, defend, and hold each Contributor harmless for any liability |
||||||
|
incurred by, or claims asserted against, such Contributor by reason of your |
||||||
|
accepting any such warranty or additional liability. |
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS |
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work |
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following boilerplate |
||||||
|
notice, with the fields enclosed by brackets "[]" replaced with your own |
||||||
|
identifying information. (Don't include the brackets!) The text should be |
||||||
|
enclosed in the appropriate comment syntax for the file format. We also |
||||||
|
recommend that a file or class name and description of purpose be included on |
||||||
|
the same "printed page" as the copyright notice for easier identification within |
||||||
|
third-party archives. |
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner] |
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
you may not use this file except in compliance with the License. |
||||||
|
You may obtain a copy of the License at |
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software |
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
See the License for the specific language governing permissions and |
||||||
|
limitations under the License. |
@ -0,0 +1,17 @@ |
|||||||
|
# Session |
||||||
|
|
||||||
|
Middleware session provides session management which copied from [Macaron Session](https://gitea.com/go-chi/session) for [go-chi](https://github.com/go-chi/chi). It can use many session providers, including memory, file, Redis, Memcache, PostgreSQL, MySQL, Couchbase, Ledis and Nodb. |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
``` |
||||||
|
go get gitea.com/go-chi/session |
||||||
|
``` |
||||||
|
|
||||||
|
## Credits |
||||||
|
|
||||||
|
This package is a modified version of [go-macaron/session](github.com/go-macaron/session). |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text. |
@ -0,0 +1,227 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
"github.com/couchbase/go-couchbase" |
||||||
|
) |
||||||
|
|
||||||
|
// CouchbaseSessionStore represents a couchbase session store implementation.
|
||||||
|
type CouchbaseSessionStore struct { |
||||||
|
b *couchbase.Bucket |
||||||
|
sid string |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
maxlifetime int64 |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *CouchbaseSessionStore) Set(key, val interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = val |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *CouchbaseSessionStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete delete a key from session.
|
||||||
|
func (s *CouchbaseSessionStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *CouchbaseSessionStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases resource and save data to provider.
|
||||||
|
func (s *CouchbaseSessionStore) Release() error { |
||||||
|
defer s.b.Close() |
||||||
|
|
||||||
|
// Skip encoding if the data is empty
|
||||||
|
if len(s.data) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
data, err := session.EncodeGob(s.data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return s.b.Set(s.sid, int(s.maxlifetime), data) |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *CouchbaseSessionStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// CouchbaseProvider represents a couchbase session provider implementation.
|
||||||
|
type CouchbaseProvider struct { |
||||||
|
maxlifetime int64 |
||||||
|
connStr string |
||||||
|
pool string |
||||||
|
bucket string |
||||||
|
b *couchbase.Bucket |
||||||
|
} |
||||||
|
|
||||||
|
func (cp *CouchbaseProvider) getBucket() *couchbase.Bucket { |
||||||
|
c, err := couchbase.Connect(cp.connStr) |
||||||
|
if err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
pool, err := c.GetPool(cp.pool) |
||||||
|
if err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
bucket, err := pool.GetBucket(cp.bucket) |
||||||
|
if err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return bucket |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes memory session provider.
|
||||||
|
// connStr is couchbase server REST/JSON URL
|
||||||
|
// e.g. http://host:port/, Pool, Bucket
|
||||||
|
func (p *CouchbaseProvider) Init(maxlifetime int64, connStr string) error { |
||||||
|
p.maxlifetime = maxlifetime |
||||||
|
configs := strings.Split(connStr, ",") |
||||||
|
if len(configs) > 0 { |
||||||
|
p.connStr = configs[0] |
||||||
|
} |
||||||
|
if len(configs) > 1 { |
||||||
|
p.pool = configs[1] |
||||||
|
} |
||||||
|
if len(configs) > 2 { |
||||||
|
p.bucket = configs[2] |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (p *CouchbaseProvider) Read(sid string) (session.RawStore, error) { |
||||||
|
p.b = p.getBucket() |
||||||
|
|
||||||
|
var doc []byte |
||||||
|
|
||||||
|
err := p.b.Get(sid, &doc) |
||||||
|
var kv map[interface{}]interface{} |
||||||
|
if doc == nil { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob(doc) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
cs := &CouchbaseSessionStore{b: p.b, sid: sid, data: kv, maxlifetime: p.maxlifetime} |
||||||
|
return cs, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (p *CouchbaseProvider) Exist(sid string) bool { |
||||||
|
p.b = p.getBucket() |
||||||
|
defer p.b.Close() |
||||||
|
|
||||||
|
var doc []byte |
||||||
|
|
||||||
|
if err := p.b.Get(sid, &doc); err != nil || doc == nil { |
||||||
|
return false |
||||||
|
} else { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (p *CouchbaseProvider) Destroy(sid string) error { |
||||||
|
p.b = p.getBucket() |
||||||
|
defer p.b.Close() |
||||||
|
|
||||||
|
p.b.Delete(sid) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (p *CouchbaseProvider) Regenerate(oldsid, sid string) (session.RawStore, error) { |
||||||
|
p.b = p.getBucket() |
||||||
|
|
||||||
|
var doc []byte |
||||||
|
if err := p.b.Get(oldsid, &doc); err != nil || doc == nil { |
||||||
|
p.b.Set(sid, int(p.maxlifetime), "") |
||||||
|
} else { |
||||||
|
err := p.b.Delete(oldsid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
_, _ = p.b.Add(sid, int(p.maxlifetime), doc) |
||||||
|
} |
||||||
|
|
||||||
|
err := p.b.Get(sid, &doc) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var kv map[interface{}]interface{} |
||||||
|
if doc == nil { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob(doc) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
cs := &CouchbaseSessionStore{b: p.b, sid: sid, data: kv, maxlifetime: p.maxlifetime} |
||||||
|
return cs, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (p *CouchbaseProvider) Count() int { |
||||||
|
// FIXME
|
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (p *CouchbaseProvider) GC() {} |
||||||
|
|
||||||
|
func init() { |
||||||
|
session.Register("couchbase", &CouchbaseProvider{}) |
||||||
|
} |
@ -0,0 +1,274 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"path" |
||||||
|
"path/filepath" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/unknwon/com" |
||||||
|
) |
||||||
|
|
||||||
|
// FileStore represents a file session store implementation.
|
||||||
|
type FileStore struct { |
||||||
|
p *FileProvider |
||||||
|
sid string |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewFileStore creates and returns a file session store.
|
||||||
|
func NewFileStore(p *FileProvider, sid string, kv map[interface{}]interface{}) *FileStore { |
||||||
|
return &FileStore{ |
||||||
|
p: p, |
||||||
|
sid: sid, |
||||||
|
data: kv, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *FileStore) Set(key, val interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = val |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *FileStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete delete a key from session.
|
||||||
|
func (s *FileStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *FileStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases resource and save data to provider.
|
||||||
|
func (s *FileStore) Release() error { |
||||||
|
s.p.lock.Lock() |
||||||
|
defer s.p.lock.Unlock() |
||||||
|
|
||||||
|
// Skip encoding if the data is empty
|
||||||
|
if len(s.data) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
data, err := EncodeGob(s.data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return ioutil.WriteFile(s.p.filepath(s.sid), data, 0600) |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *FileStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// FileProvider represents a file session provider implementation.
|
||||||
|
type FileProvider struct { |
||||||
|
lock sync.RWMutex |
||||||
|
maxlifetime int64 |
||||||
|
rootPath string |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes file session provider with given root path.
|
||||||
|
func (p *FileProvider) Init(maxlifetime int64, rootPath string) error { |
||||||
|
p.lock.Lock() |
||||||
|
p.maxlifetime = maxlifetime |
||||||
|
p.rootPath = rootPath |
||||||
|
p.lock.Unlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *FileProvider) filepath(sid string) string { |
||||||
|
return path.Join(p.rootPath, string(sid[0]), string(sid[1]), sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (p *FileProvider) Read(sid string) (_ RawStore, err error) { |
||||||
|
filename := p.filepath(sid) |
||||||
|
if err = os.MkdirAll(path.Dir(filename), 0700); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
p.lock.RLock() |
||||||
|
defer p.lock.RUnlock() |
||||||
|
|
||||||
|
var f *os.File |
||||||
|
ok := false |
||||||
|
if com.IsFile(filename) { |
||||||
|
modTime, err := com.FileMTime(filename) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
ok = (modTime + p.maxlifetime) >= time.Now().Unix() |
||||||
|
} |
||||||
|
if ok { |
||||||
|
f, err = os.OpenFile(filename, os.O_RDONLY, 0600) |
||||||
|
} else { |
||||||
|
f, err = os.Create(filename) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
|
||||||
|
if err = os.Chtimes(filename, time.Now(), time.Now()); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var kv map[interface{}]interface{} |
||||||
|
data, err := ioutil.ReadAll(f) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(data) == 0 { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = DecodeGob(data) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
return NewFileStore(p, sid, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (p *FileProvider) Exist(sid string) bool { |
||||||
|
p.lock.RLock() |
||||||
|
defer p.lock.RUnlock() |
||||||
|
return com.IsFile(p.filepath(sid)) |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (p *FileProvider) Destroy(sid string) error { |
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
return os.Remove(p.filepath(sid)) |
||||||
|
} |
||||||
|
|
||||||
|
func (p *FileProvider) regenerate(oldsid, sid string) (err error) { |
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
|
||||||
|
filename := p.filepath(sid) |
||||||
|
if com.IsExist(filename) { |
||||||
|
return fmt.Errorf("new sid '%s' already exists", sid) |
||||||
|
} |
||||||
|
|
||||||
|
oldname := p.filepath(oldsid) |
||||||
|
if !com.IsFile(oldname) { |
||||||
|
data, err := EncodeGob(make(map[interface{}]interface{})) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err = os.MkdirAll(path.Dir(oldname), 0700); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err = ioutil.WriteFile(oldname, data, 0600); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err = os.MkdirAll(path.Dir(filename), 0700); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err = os.Rename(oldname, filename); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (p *FileProvider) Regenerate(oldsid, sid string) (_ RawStore, err error) { |
||||||
|
if err := p.regenerate(oldsid, sid); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return p.Read(sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (p *FileProvider) Count() int { |
||||||
|
count := 0 |
||||||
|
if err := filepath.Walk(p.rootPath, func(path string, fi os.FileInfo, err error) error { |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if !fi.IsDir() { |
||||||
|
count++ |
||||||
|
} |
||||||
|
return nil |
||||||
|
}); err != nil { |
||||||
|
log.Printf("error counting session files: %v", err) |
||||||
|
return 0 |
||||||
|
} |
||||||
|
return count |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (p *FileProvider) GC() { |
||||||
|
p.lock.RLock() |
||||||
|
defer p.lock.RUnlock() |
||||||
|
|
||||||
|
if !com.IsExist(p.rootPath) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err := filepath.Walk(p.rootPath, func(path string, fi os.FileInfo, err error) error { |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if !fi.IsDir() && |
||||||
|
(fi.ModTime().Unix()+p.maxlifetime) < time.Now().Unix() { |
||||||
|
return os.Remove(path) |
||||||
|
} |
||||||
|
return nil |
||||||
|
}); err != nil { |
||||||
|
log.Printf("error garbage collecting session files: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
Register("file", &FileProvider{}) |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
module gitea.com/go-chi/session |
||||||
|
|
||||||
|
go 1.11 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 |
||||||
|
github.com/couchbase/go-couchbase v0.0.0-20201026062457-7b3be89bbd89 |
||||||
|
github.com/couchbase/gomemcached v0.1.1 // indirect |
||||||
|
github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67 // indirect |
||||||
|
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76 // indirect |
||||||
|
github.com/edsrzf/mmap-go v1.0.0 // indirect |
||||||
|
github.com/go-chi/chi v1.5.1 |
||||||
|
github.com/go-redis/redis/v8 v8.4.0 |
||||||
|
github.com/go-sql-driver/mysql v1.4.1 |
||||||
|
github.com/lib/pq v1.2.0 |
||||||
|
github.com/pelletier/go-toml v1.8.1 // indirect |
||||||
|
github.com/pkg/errors v0.9.1 // indirect |
||||||
|
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect |
||||||
|
github.com/siddontang/ledisdb v0.0.0-20190202134119-8ceb77e66a92 |
||||||
|
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d // indirect |
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 |
||||||
|
github.com/syndtr/goleveldb v1.0.0 // indirect |
||||||
|
github.com/unknwon/com v1.0.1 |
||||||
|
google.golang.org/appengine v1.6.7 // indirect |
||||||
|
gopkg.in/ini.v1 v1.62.0 |
||||||
|
) |
@ -0,0 +1,151 @@ |
|||||||
|
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA= |
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= |
||||||
|
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= |
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= |
||||||
|
github.com/couchbase/go-couchbase v0.0.0-20201026062457-7b3be89bbd89 h1:uNLXQ6QO1TocD8BaN/KkRki0Xw0brCM1PKl/ZA5pgfs= |
||||||
|
github.com/couchbase/go-couchbase v0.0.0-20201026062457-7b3be89bbd89/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= |
||||||
|
github.com/couchbase/gomemcached v0.1.1 h1:xCS8ZglJDhrlQg3jmK7Rn1V8f7bPjXABLC05CgLQauc= |
||||||
|
github.com/couchbase/gomemcached v0.1.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= |
||||||
|
github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67 h1:NCqJ6fwen6YP0WlV/IyibaT0kPt3JEI1rA62V/UPKT4= |
||||||
|
github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= |
||||||
|
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76 h1:Lgdd/Qp96Qj8jqLpq2cI1I1X7BJnu06efS+XkhRoLUQ= |
||||||
|
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= |
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= |
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= |
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= |
||||||
|
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= |
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= |
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= |
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= |
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= |
||||||
|
github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w= |
||||||
|
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k= |
||||||
|
github.com/go-redis/redis/v8 v8.4.0 h1:J5NCReIgh3QgUJu398hUncxDExN4gMOHI11NVbVicGQ= |
||||||
|
github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= |
||||||
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= |
||||||
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= |
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||||
|
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= |
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= |
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= |
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= |
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= |
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= |
||||||
|
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= |
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= |
||||||
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= |
||||||
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= |
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= |
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
||||||
|
github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= |
||||||
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= |
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= |
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= |
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= |
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
||||||
|
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= |
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= |
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= |
||||||
|
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= |
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= |
||||||
|
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= |
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= |
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
||||||
|
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= |
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= |
||||||
|
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= |
||||||
|
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= |
||||||
|
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= |
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= |
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= |
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= |
||||||
|
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= |
||||||
|
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= |
||||||
|
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= |
||||||
|
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= |
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= |
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||||
|
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM= |
||||||
|
github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= |
||||||
|
github.com/siddontang/ledisdb v0.0.0-20190202134119-8ceb77e66a92 h1:qvsJwGToa8rxb42cDRhkbKeX2H5N8BH+s2aUikGt8mI= |
||||||
|
github.com/siddontang/ledisdb v0.0.0-20190202134119-8ceb77e66a92/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg= |
||||||
|
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d h1:NVwnfyR3rENtlz62bcrkXME3INVUa4lcdGt+opvxExs= |
||||||
|
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= |
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= |
||||||
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= |
||||||
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= |
||||||
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= |
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= |
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= |
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= |
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
||||||
|
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= |
||||||
|
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= |
||||||
|
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= |
||||||
|
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= |
||||||
|
go.opentelemetry.io/otel v0.14.0 h1:YFBEfjCk9MTjaytCNSUkp9Q8lF7QJezA06T71FbQxLQ= |
||||||
|
go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= |
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= |
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= |
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
||||||
|
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= |
||||||
|
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= |
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= |
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= |
||||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= |
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= |
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= |
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= |
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= |
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= |
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= |
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= |
||||||
|
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= |
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= |
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
||||||
|
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= |
||||||
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= |
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= |
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= |
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= |
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
@ -0,0 +1,203 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
"github.com/bradfitz/gomemcache/memcache" |
||||||
|
) |
||||||
|
|
||||||
|
// MemcacheStore represents a memcache session store implementation.
|
||||||
|
type MemcacheStore struct { |
||||||
|
c *memcache.Client |
||||||
|
sid string |
||||||
|
expire int32 |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewMemcacheStore creates and returns a memcache session store.
|
||||||
|
func NewMemcacheStore(c *memcache.Client, sid string, expire int32, kv map[interface{}]interface{}) *MemcacheStore { |
||||||
|
return &MemcacheStore{ |
||||||
|
c: c, |
||||||
|
sid: sid, |
||||||
|
expire: expire, |
||||||
|
data: kv, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func NewItem(sid string, data []byte, expire int32) *memcache.Item { |
||||||
|
return &memcache.Item{ |
||||||
|
Key: sid, |
||||||
|
Value: data, |
||||||
|
Expiration: expire, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *MemcacheStore) Set(key, val interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = val |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *MemcacheStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete delete a key from session.
|
||||||
|
func (s *MemcacheStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *MemcacheStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases resource and save data to provider.
|
||||||
|
func (s *MemcacheStore) Release() error { |
||||||
|
// Skip encoding if the data is empty
|
||||||
|
if len(s.data) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
data, err := session.EncodeGob(s.data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return s.c.Set(NewItem(s.sid, data, s.expire)) |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *MemcacheStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// MemcacheProvider represents a memcache session provider implementation.
|
||||||
|
type MemcacheProvider struct { |
||||||
|
c *memcache.Client |
||||||
|
expire int32 |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes memcache session provider.
|
||||||
|
// connStrs: 127.0.0.1:9090;127.0.0.1:9091
|
||||||
|
func (p *MemcacheProvider) Init(expire int64, connStrs string) error { |
||||||
|
p.expire = int32(expire) |
||||||
|
p.c = memcache.New(strings.Split(connStrs, ";")...) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (p *MemcacheProvider) Read(sid string) (session.RawStore, error) { |
||||||
|
if !p.Exist(sid) { |
||||||
|
if err := p.c.Set(NewItem(sid, []byte(""), p.expire)); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var kv map[interface{}]interface{} |
||||||
|
item, err := p.c.Get(sid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(item.Value) == 0 { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob(item.Value) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NewMemcacheStore(p.c, sid, p.expire, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (p *MemcacheProvider) Exist(sid string) bool { |
||||||
|
_, err := p.c.Get(sid) |
||||||
|
return err == nil |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (p *MemcacheProvider) Destroy(sid string) error { |
||||||
|
return p.c.Delete(sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (p *MemcacheProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { |
||||||
|
if p.Exist(sid) { |
||||||
|
return nil, fmt.Errorf("new sid '%s' already exists", sid) |
||||||
|
} |
||||||
|
|
||||||
|
item := NewItem(sid, []byte(""), p.expire) |
||||||
|
if p.Exist(oldsid) { |
||||||
|
item, err = p.c.Get(oldsid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} else if err = p.c.Delete(oldsid); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
item.Key = sid |
||||||
|
} |
||||||
|
if err = p.c.Set(item); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var kv map[interface{}]interface{} |
||||||
|
if len(item.Value) == 0 { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob(item.Value) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NewMemcacheStore(p.c, sid, p.expire, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (p *MemcacheProvider) Count() int { |
||||||
|
// FIXME: how come this library does not have Stats method?
|
||||||
|
return -1 |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (p *MemcacheProvider) GC() {} |
||||||
|
|
||||||
|
func init() { |
||||||
|
session.Register("memcache", &MemcacheProvider{}) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
ignore |
@ -0,0 +1,223 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"container/list" |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// MemStore represents a in-memory session store implementation.
|
||||||
|
type MemStore struct { |
||||||
|
sid string |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
lastAccess time.Time |
||||||
|
} |
||||||
|
|
||||||
|
// NewMemStore creates and returns a memory session store.
|
||||||
|
func NewMemStore(sid string) *MemStore { |
||||||
|
return &MemStore{ |
||||||
|
sid: sid, |
||||||
|
data: make(map[interface{}]interface{}), |
||||||
|
lastAccess: time.Now(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *MemStore) Set(key, val interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = val |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *MemStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete deletes a key from session.
|
||||||
|
func (s *MemStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *MemStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases resource and save data to provider.
|
||||||
|
func (*MemStore) Release() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *MemStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// MemProvider represents a in-memory session provider implementation.
|
||||||
|
type MemProvider struct { |
||||||
|
lock sync.RWMutex |
||||||
|
maxLifetime int64 |
||||||
|
data map[string]*list.Element |
||||||
|
// A priority list whose lastAccess newer gets higer priority.
|
||||||
|
list *list.List |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes memory session provider.
|
||||||
|
func (p *MemProvider) Init(maxLifetime int64, _ string) error { |
||||||
|
p.lock.Lock() |
||||||
|
p.list = list.New() |
||||||
|
p.data = make(map[string]*list.Element) |
||||||
|
p.maxLifetime = maxLifetime |
||||||
|
p.lock.Unlock() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// update expands time of session store by given ID.
|
||||||
|
func (p *MemProvider) update(sid string) error { |
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
|
||||||
|
if e, ok := p.data[sid]; ok { |
||||||
|
e.Value.(*MemStore).lastAccess = time.Now() |
||||||
|
p.list.MoveToFront(e) |
||||||
|
return nil |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (p *MemProvider) Read(sid string) (_ RawStore, err error) { |
||||||
|
p.lock.RLock() |
||||||
|
e, ok := p.data[sid] |
||||||
|
p.lock.RUnlock() |
||||||
|
|
||||||
|
// Only restore if the session is still alive.
|
||||||
|
if ok && (e.Value.(*MemStore).lastAccess.Unix()+p.maxLifetime) >= time.Now().Unix() { |
||||||
|
if err = p.update(sid); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return e.Value.(*MemStore), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new session.
|
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
if ok { |
||||||
|
p.list.Remove(e) |
||||||
|
delete(p.data, sid) |
||||||
|
} |
||||||
|
s := NewMemStore(sid) |
||||||
|
p.data[sid] = p.list.PushBack(s) |
||||||
|
return s, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (p *MemProvider) Exist(sid string) bool { |
||||||
|
p.lock.RLock() |
||||||
|
defer p.lock.RUnlock() |
||||||
|
|
||||||
|
_, ok := p.data[sid] |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (p *MemProvider) Destroy(sid string) error { |
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
|
||||||
|
e, ok := p.data[sid] |
||||||
|
if !ok { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
p.list.Remove(e) |
||||||
|
delete(p.data, sid) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (p *MemProvider) Regenerate(oldsid, sid string) (RawStore, error) { |
||||||
|
if p.Exist(sid) { |
||||||
|
return nil, fmt.Errorf("new sid '%s' already exists", sid) |
||||||
|
} |
||||||
|
|
||||||
|
s, err := p.Read(oldsid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if err = p.Destroy(oldsid); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
s.(*MemStore).sid = sid |
||||||
|
|
||||||
|
p.lock.Lock() |
||||||
|
defer p.lock.Unlock() |
||||||
|
p.data[sid] = p.list.PushBack(s) |
||||||
|
return s, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (p *MemProvider) Count() int { |
||||||
|
return p.list.Len() |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (p *MemProvider) GC() { |
||||||
|
p.lock.RLock() |
||||||
|
for { |
||||||
|
// No session in the list.
|
||||||
|
e := p.list.Back() |
||||||
|
if e == nil { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
if (e.Value.(*MemStore).lastAccess.Unix() + p.maxLifetime) < time.Now().Unix() { |
||||||
|
p.lock.RUnlock() |
||||||
|
p.lock.Lock() |
||||||
|
p.list.Remove(e) |
||||||
|
delete(p.data, e.Value.(*MemStore).sid) |
||||||
|
p.lock.Unlock() |
||||||
|
p.lock.RLock() |
||||||
|
} else { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
p.lock.RUnlock() |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
Register("memory", &MemProvider{}) |
||||||
|
} |
@ -0,0 +1,201 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
_ "github.com/go-sql-driver/mysql" |
||||||
|
) |
||||||
|
|
||||||
|
// MysqlStore represents a mysql session store implementation.
|
||||||
|
type MysqlStore struct { |
||||||
|
c *sql.DB |
||||||
|
sid string |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewMysqlStore creates and returns a mysql session store.
|
||||||
|
func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore { |
||||||
|
return &MysqlStore{ |
||||||
|
c: c, |
||||||
|
sid: sid, |
||||||
|
data: kv, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *MysqlStore) Set(key, val interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = val |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *MysqlStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete delete a key from session.
|
||||||
|
func (s *MysqlStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *MysqlStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// Release releases resource and save data to provider.
|
||||||
|
func (s *MysqlStore) Release() error { |
||||||
|
// Skip encoding if the data is empty
|
||||||
|
if len(s.data) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
data, err := session.EncodeGob(s.data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?", |
||||||
|
data, time.Now().Unix(), s.sid) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *MysqlStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// MysqlProvider represents a mysql session provider implementation.
|
||||||
|
type MysqlProvider struct { |
||||||
|
c *sql.DB |
||||||
|
expire int64 |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes mysql session provider.
|
||||||
|
// connStr: username:password@protocol(address)/dbname?param=value
|
||||||
|
func (p *MysqlProvider) Init(expire int64, connStr string) (err error) { |
||||||
|
p.expire = expire |
||||||
|
|
||||||
|
p.c, err = sql.Open("mysql", connStr) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return p.c.Ping() |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { |
||||||
|
now := time.Now().Unix() |
||||||
|
var data []byte |
||||||
|
expiry := now |
||||||
|
err := p.c.QueryRow("SELECT data, expiry FROM session WHERE `key`=?", sid).Scan(&data, &expiry) |
||||||
|
if err == sql.ErrNoRows { |
||||||
|
_, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", |
||||||
|
sid, "", now) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var kv map[interface{}]interface{} |
||||||
|
if len(data) == 0 || expiry+p.expire <= now { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob(data) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NewMysqlStore(p.c, sid, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (p *MysqlProvider) Exist(sid string) bool { |
||||||
|
var data []byte |
||||||
|
err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) |
||||||
|
if err != nil && err != sql.ErrNoRows { |
||||||
|
panic("session/mysql: error checking existence: " + err.Error()) |
||||||
|
} |
||||||
|
return err != sql.ErrNoRows |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (p *MysqlProvider) Destroy(sid string) error { |
||||||
|
_, err := p.c.Exec("DELETE FROM session WHERE `key`=?", sid) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (p *MysqlProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { |
||||||
|
if p.Exist(sid) { |
||||||
|
return nil, fmt.Errorf("new sid '%s' already exists", sid) |
||||||
|
} |
||||||
|
|
||||||
|
if !p.Exist(oldsid) { |
||||||
|
if _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", |
||||||
|
oldsid, "", time.Now().Unix()); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if _, err = p.c.Exec("UPDATE session SET `key`=? WHERE `key`=?", sid, oldsid); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return p.Read(sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (p *MysqlProvider) Count() (total int) { |
||||||
|
if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil { |
||||||
|
panic("session/mysql: error counting records: " + err.Error()) |
||||||
|
} |
||||||
|
return total |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (p *MysqlProvider) GC() { |
||||||
|
if _, err := p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil { |
||||||
|
log.Printf("session/mysql: error garbage collecting: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
session.Register("mysql", &MysqlProvider{}) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
ignore |
@ -0,0 +1,202 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"gitea.com/go-chi/session" |
||||||
|
_ "github.com/lib/pq" |
||||||
|
) |
||||||
|
|
||||||
|
// PostgresStore represents a postgres session store implementation.
|
||||||
|
type PostgresStore struct { |
||||||
|
c *sql.DB |
||||||
|
sid string |
||||||
|
lock sync.RWMutex |
||||||
|
data map[interface{}]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewPostgresStore creates and returns a postgres session store.
|
||||||
|
func NewPostgresStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *PostgresStore { |
||||||
|
return &PostgresStore{ |
||||||
|
c: c, |
||||||
|
sid: sid, |
||||||
|
data: kv, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets value to given key in session.
|
||||||
|
func (s *PostgresStore) Set(key, value interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data[key] = value |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets value by given key in session.
|
||||||
|
func (s *PostgresStore) Get(key interface{}) interface{} { |
||||||
|
s.lock.RLock() |
||||||
|
defer s.lock.RUnlock() |
||||||
|
|
||||||
|
return s.data[key] |
||||||
|
} |
||||||
|
|
||||||
|
// Delete delete a key from session.
|
||||||
|
func (s *PostgresStore) Delete(key interface{}) error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
delete(s.data, key) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ID returns current session ID.
|
||||||
|
func (s *PostgresStore) ID() string { |
||||||
|
return s.sid |
||||||
|
} |
||||||
|
|
||||||
|
// save postgres session values to database.
|
||||||
|
// must call this method to save values to database.
|
||||||
|
func (s *PostgresStore) Release() error { |
||||||
|
// Skip encoding if the data is empty
|
||||||
|
if len(s.data) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
data, err := session.EncodeGob(s.data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = s.c.Exec("UPDATE session SET data=$1, expiry=$2 WHERE key=$3", |
||||||
|
data, time.Now().Unix(), s.sid) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Flush deletes all session data.
|
||||||
|
func (s *PostgresStore) Flush() error { |
||||||
|
s.lock.Lock() |
||||||
|
defer s.lock.Unlock() |
||||||
|
|
||||||
|
s.data = make(map[interface{}]interface{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// PostgresProvider represents a postgres session provider implementation.
|
||||||
|
type PostgresProvider struct { |
||||||
|
c *sql.DB |
||||||
|
maxlifetime int64 |
||||||
|
} |
||||||
|
|
||||||
|
// Init initializes postgres session provider.
|
||||||
|
// connStr: user=a password=b host=localhost port=5432 dbname=c sslmode=disable
|
||||||
|
func (p *PostgresProvider) Init(maxlifetime int64, connStr string) (err error) { |
||||||
|
p.maxlifetime = maxlifetime |
||||||
|
|
||||||
|
p.c, err = sql.Open("postgres", connStr) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return p.c.Ping() |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (p *PostgresProvider) Read(sid string) (session.RawStore, error) { |
||||||
|
now := time.Now().Unix() |
||||||
|
var data []byte |
||||||
|
expiry := now |
||||||
|
err := p.c.QueryRow("SELECT data, expiry FROM session WHERE key=$1", sid).Scan(&data, &expiry) |
||||||
|
if err == sql.ErrNoRows { |
||||||
|
_, err = p.c.Exec("INSERT INTO session(key,data,expiry) VALUES($1,$2,$3)", |
||||||
|
sid, "", now) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var kv map[interface{}]interface{} |
||||||
|
if len(data) == 0 || expiry+p.maxlifetime <= now { |
||||||
|
kv = make(map[interface{}]interface{}) |
||||||
|
} else { |
||||||
|
kv, err = session.DecodeGob(data) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return NewPostgresStore(p.c, sid, kv), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
func (p *PostgresProvider) Exist(sid string) bool { |
||||||
|
var data []byte |
||||||
|
err := p.c.QueryRow("SELECT data FROM session WHERE key=$1", sid).Scan(&data) |
||||||
|
if err != nil && err != sql.ErrNoRows { |
||||||
|
panic("session/postgres: error checking existence: " + err.Error()) |
||||||
|
} |
||||||
|
return err != sql.ErrNoRows |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
func (p *PostgresProvider) Destroy(sid string) error { |
||||||
|
_, err := p.c.Exec("DELETE FROM session WHERE key=$1", sid) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
func (p *PostgresProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { |
||||||
|
if p.Exist(sid) { |
||||||
|
return nil, fmt.Errorf("new sid '%s' already exists", sid) |
||||||
|
} |
||||||
|
|
||||||
|
if !p.Exist(oldsid) { |
||||||
|
if _, err = p.c.Exec("INSERT INTO session(key,data,expiry) VALUES($1,$2,$3)", |
||||||
|
oldsid, "", time.Now().Unix()); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if _, err = p.c.Exec("UPDATE session SET key=$1 WHERE key=$2", sid, oldsid); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return p.Read(sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (p *PostgresProvider) Count() (total int) { |
||||||
|
if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil { |
||||||
|
panic("session/postgres: error counting records: " + err.Error()) |
||||||
|
} |
||||||
|
return total |
||||||
|
} |
||||||
|
|
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
func (p *PostgresProvider) GC() { |
||||||
|
if _, err := p.c.Exec("DELETE FROM session WHERE EXTRACT(EPOCH FROM NOW()) - expiry > $1", p.maxlifetime); err != nil { |
||||||
|
log.Printf("session/postgres: error garbage collecting: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
session.Register("postgres", &PostgresProvider{}) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
ignore |
@ -0,0 +1,100 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/aes" |
||||||
|
"crypto/cipher" |
||||||
|
"crypto/rand" |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/base64" |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
) |
||||||
|
|
||||||
|
// NewSecret creates a new secret
|
||||||
|
func NewSecret() (string, error) { |
||||||
|
return NewSecretWithLength(32) |
||||||
|
} |
||||||
|
|
||||||
|
// NewSecretWithLength creates a new secret for a given length
|
||||||
|
func NewSecretWithLength(length int64) (string, error) { |
||||||
|
return randomString(length) |
||||||
|
} |
||||||
|
|
||||||
|
func randomBytes(len int64) ([]byte, error) { |
||||||
|
b := make([]byte, len) |
||||||
|
if _, err := rand.Read(b); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return b, nil |
||||||
|
} |
||||||
|
|
||||||
|
func randomString(len int64) (string, error) { |
||||||
|
b, err := randomBytes(len) |
||||||
|
return base64.URLEncoding.EncodeToString(b), err |
||||||
|
} |
||||||
|
|
||||||
|
// AesEncrypt encrypts text and given key with AES.
|
||||||
|
func AesEncrypt(key, text []byte) ([]byte, error) { |
||||||
|
block, err := aes.NewCipher(key) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
b := base64.StdEncoding.EncodeToString(text) |
||||||
|
ciphertext := make([]byte, aes.BlockSize+len(b)) |
||||||
|
iv := ciphertext[:aes.BlockSize] |
||||||
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
cfb := cipher.NewCFBEncrypter(block, iv) |
||||||
|
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b)) |
||||||
|
return ciphertext, nil |
||||||
|
} |
||||||
|
|
||||||
|
// AesDecrypt decrypts text and given key with AES.
|
||||||
|
func AesDecrypt(key, text []byte) ([]byte, error) { |
||||||
|
block, err := aes.NewCipher(key) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(text) < aes.BlockSize { |
||||||
|
return nil, errors.New("ciphertext too short") |
||||||
|
} |
||||||
|
iv := text[:aes.BlockSize] |
||||||
|
text = text[aes.BlockSize:] |
||||||
|
cfb := cipher.NewCFBDecrypter(block, iv) |
||||||
|
cfb.XORKeyStream(text, text) |
||||||
|
data, err := base64.StdEncoding.DecodeString(string(text)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return data, nil |
||||||
|
} |
||||||
|
|
||||||
|
// EncryptSecret encrypts a string with given key into a hex string
|
||||||
|
func EncryptSecret(key string, str string) (string, error) { |
||||||
|
keyHash := sha256.Sum256([]byte(key)) |
||||||
|
plaintext := []byte(str) |
||||||
|
ciphertext, err := AesEncrypt(keyHash[:], plaintext) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return base64.StdEncoding.EncodeToString(ciphertext), nil |
||||||
|
} |
||||||
|
|
||||||
|
// DecryptSecret decrypts a previously encrypted hex string
|
||||||
|
func DecryptSecret(key string, cipherhex string) (string, error) { |
||||||
|
keyHash := sha256.Sum256([]byte(key)) |
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(cipherhex) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
plaintext, err := AesDecrypt(keyHash[:], ciphertext) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return string(plaintext), nil |
||||||
|
} |
@ -0,0 +1,466 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
// Package session a middleware that provides the session management of Macaron.
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/hex" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const version = "0.7.0" |
||||||
|
|
||||||
|
// Version returns the version
|
||||||
|
func Version() string { |
||||||
|
return version |
||||||
|
} |
||||||
|
|
||||||
|
// RawStore is the interface that operates the session data.
|
||||||
|
type RawStore interface { |
||||||
|
// Set sets value to given key in session.
|
||||||
|
Set(interface{}, interface{}) error |
||||||
|
// Get gets value by given key in session.
|
||||||
|
Get(interface{}) interface{} |
||||||
|
// Delete deletes a key from session.
|
||||||
|
Delete(interface{}) error |
||||||
|
// ID returns current session ID.
|
||||||
|
ID() string |
||||||
|
// Release releases session resource and save data to provider.
|
||||||
|
Release() error |
||||||
|
// Flush deletes all session data.
|
||||||
|
Flush() error |
||||||
|
} |
||||||
|
|
||||||
|
// Store is the interface that contains all data for one session process with specific ID.
|
||||||
|
type Store interface { |
||||||
|
RawStore |
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
Read(string) (RawStore, error) |
||||||
|
// Destroy deletes a session.
|
||||||
|
Destroy(http.ResponseWriter, *http.Request) error |
||||||
|
// RegenerateID regenerates a session store from old session ID to new one.
|
||||||
|
RegenerateID(http.ResponseWriter, *http.Request) (RawStore, error) |
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
Count() int |
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
GC() |
||||||
|
} |
||||||
|
|
||||||
|
type store struct { |
||||||
|
RawStore |
||||||
|
*Manager |
||||||
|
} |
||||||
|
|
||||||
|
var _ Store = &store{} |
||||||
|
|
||||||
|
// Options represents a struct for specifying configuration options for the session middleware.
|
||||||
|
type Options struct { |
||||||
|
// Name of provider. Default is "memory".
|
||||||
|
Provider string |
||||||
|
// Provider configuration, it's corresponding to provider.
|
||||||
|
ProviderConfig string |
||||||
|
// Cookie name to save session ID. Default is "MacaronSession".
|
||||||
|
CookieName string |
||||||
|
// Cookie path to store. Default is "/".
|
||||||
|
CookiePath string |
||||||
|
// GC interval time in seconds. Default is 3600.
|
||||||
|
Gclifetime int64 |
||||||
|
// Max life time in seconds. Default is whatever GC interval time is.
|
||||||
|
Maxlifetime int64 |
||||||
|
// Use HTTPS only. Default is false.
|
||||||
|
Secure bool |
||||||
|
// Cookie life time. Default is 0.
|
||||||
|
CookieLifeTime int |
||||||
|
// SameSite set the cookie SameSite
|
||||||
|
SameSite http.SameSite |
||||||
|
// Cookie domain name. Default is empty.
|
||||||
|
Domain string |
||||||
|
// Session ID length. Default is 16.
|
||||||
|
IDLength int |
||||||
|
// Ignore release for websocket. Default is false.
|
||||||
|
IgnoreReleaseForWebSocket bool |
||||||
|
// FlashEncryptionKey sets the encryption key for flash messages
|
||||||
|
FlashEncryptionKey string |
||||||
|
} |
||||||
|
|
||||||
|
func prepareOptions(options []Options) Options { |
||||||
|
var opt Options |
||||||
|
if len(options) > 0 { |
||||||
|
opt = options[0] |
||||||
|
} |
||||||
|
|
||||||
|
if len(opt.Provider) == 0 { |
||||||
|
opt.Provider = "memory" |
||||||
|
} |
||||||
|
if len(opt.ProviderConfig) == 0 { |
||||||
|
opt.ProviderConfig = "data/sessions" |
||||||
|
} |
||||||
|
if len(opt.CookieName) == 0 { |
||||||
|
opt.CookieName = "MacaronSession" |
||||||
|
} |
||||||
|
if len(opt.CookiePath) == 0 { |
||||||
|
opt.CookiePath = "/" |
||||||
|
} |
||||||
|
if opt.Gclifetime == 0 { |
||||||
|
opt.Gclifetime = 3600 |
||||||
|
} |
||||||
|
if opt.Maxlifetime == 0 { |
||||||
|
opt.Maxlifetime = opt.Gclifetime |
||||||
|
} |
||||||
|
if !opt.Secure { |
||||||
|
opt.Secure = false |
||||||
|
} |
||||||
|
if opt.IDLength == 0 { |
||||||
|
opt.IDLength = 16 |
||||||
|
} |
||||||
|
if len(opt.FlashEncryptionKey) == 0 { |
||||||
|
opt.FlashEncryptionKey = "" |
||||||
|
} |
||||||
|
if len(opt.FlashEncryptionKey) == 0 { |
||||||
|
opt.FlashEncryptionKey, _ = NewSecret() |
||||||
|
} |
||||||
|
|
||||||
|
return opt |
||||||
|
} |
||||||
|
|
||||||
|
// GetCookie returns given cookie value from request header.
|
||||||
|
func GetCookie(req *http.Request, name string) string { |
||||||
|
cookie, err := req.Cookie(name) |
||||||
|
if err != nil { |
||||||
|
return "" |
||||||
|
} |
||||||
|
val, _ := url.QueryUnescape(cookie.Value) |
||||||
|
return val |
||||||
|
} |
||||||
|
|
||||||
|
// NewCookie creates cookie via given params and value.
|
||||||
|
// FIXME: IE support? http://golanghome.com/post/620#reply2
|
||||||
|
func NewCookie(name string, value string, others ...interface{}) *http.Cookie { |
||||||
|
cookie := http.Cookie{} |
||||||
|
cookie.Name = name |
||||||
|
cookie.Value = url.QueryEscape(value) |
||||||
|
|
||||||
|
if len(others) > 0 { |
||||||
|
switch v := others[0].(type) { |
||||||
|
case int: |
||||||
|
cookie.MaxAge = v |
||||||
|
case int64: |
||||||
|
cookie.MaxAge = int(v) |
||||||
|
case int32: |
||||||
|
cookie.MaxAge = int(v) |
||||||
|
case func(*http.Cookie): |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
cookie.Path = "/" |
||||||
|
if len(others) > 1 { |
||||||
|
if v, ok := others[1].(string); ok && len(v) > 0 { |
||||||
|
cookie.Path = v |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 2 { |
||||||
|
if v, ok := others[2].(string); ok && len(v) > 0 { |
||||||
|
cookie.Domain = v |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 3 { |
||||||
|
switch v := others[3].(type) { |
||||||
|
case bool: |
||||||
|
cookie.Secure = v |
||||||
|
case func(*http.Cookie): |
||||||
|
v(&cookie) |
||||||
|
default: |
||||||
|
if others[3] != nil { |
||||||
|
cookie.Secure = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 4 { |
||||||
|
if v, ok := others[4].(bool); ok && v { |
||||||
|
cookie.HttpOnly = true |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 5 { |
||||||
|
if v, ok := others[5].(time.Time); ok { |
||||||
|
cookie.Expires = v |
||||||
|
cookie.RawExpires = v.Format(time.UnixDate) |
||||||
|
} else if v, ok := others[1].(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(others) > 6 { |
||||||
|
for _, other := range others[6:] { |
||||||
|
if v, ok := other.(func(*http.Cookie)); ok { |
||||||
|
v(&cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return &cookie |
||||||
|
} |
||||||
|
|
||||||
|
// Sessioner is a middleware that maps a session.SessionStore service into the Macaron handler chain.
|
||||||
|
// An single variadic session.Options struct can be optionally provided to configure.
|
||||||
|
func Sessioner(options ...Options) func(next http.Handler) http.Handler { |
||||||
|
opt := prepareOptions(options) |
||||||
|
manager, err := NewManager(opt.Provider, opt) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
go manager.startGC() |
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler { |
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
||||||
|
sess, err := manager.Start(w, req) |
||||||
|
if err != nil { |
||||||
|
panic("session(start): " + err.Error()) |
||||||
|
} |
||||||
|
|
||||||
|
var s = store{ |
||||||
|
RawStore: sess, |
||||||
|
Manager: manager, |
||||||
|
} |
||||||
|
|
||||||
|
req = req.WithContext(context.WithValue(req.Context(), interface{}("Session"), &s)) |
||||||
|
|
||||||
|
next.ServeHTTP(w, req) |
||||||
|
|
||||||
|
if manager.opt.IgnoreReleaseForWebSocket && req.Header.Get("Upgrade") == "websocket" { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err = sess.Release(); err != nil { |
||||||
|
panic("session(release): " + err.Error()) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// GetSession returns session store
|
||||||
|
func GetSession(req *http.Request) Store { |
||||||
|
sessCtx := req.Context().Value("Session") |
||||||
|
sess, _ := sessCtx.(*store) |
||||||
|
return sess |
||||||
|
} |
||||||
|
|
||||||
|
// Provider is the interface that provides session manipulations.
|
||||||
|
type Provider interface { |
||||||
|
// Init initializes session provider.
|
||||||
|
Init(gclifetime int64, config string) error |
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
Read(sid string) (RawStore, error) |
||||||
|
// Exist returns true if session with given ID exists.
|
||||||
|
Exist(sid string) bool |
||||||
|
// Destroy deletes a session by session ID.
|
||||||
|
Destroy(sid string) error |
||||||
|
// Regenerate regenerates a session store from old session ID to new one.
|
||||||
|
Regenerate(oldsid, sid string) (RawStore, error) |
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
Count() int |
||||||
|
// GC calls GC to clean expired sessions.
|
||||||
|
GC() |
||||||
|
} |
||||||
|
|
||||||
|
var providers = make(map[string]Provider) |
||||||
|
|
||||||
|
// Register registers a provider.
|
||||||
|
func Register(name string, provider Provider) { |
||||||
|
if provider == nil { |
||||||
|
panic("session: cannot register provider with nil value") |
||||||
|
} |
||||||
|
if _, dup := providers[name]; dup { |
||||||
|
panic(fmt.Errorf("session: cannot register provider '%s' twice", name)) |
||||||
|
} |
||||||
|
providers[name] = provider |
||||||
|
} |
||||||
|
|
||||||
|
// _____
|
||||||
|
// / \ _____ ____ _____ ____ ___________
|
||||||
|
// / \ / \\__ \ / \\__ \ / ___\_/ __ \_ __ \
|
||||||
|
// / Y \/ __ \| | \/ __ \_/ /_/ > ___/| | \/
|
||||||
|
// \____|__ (____ /___| (____ /\___ / \___ >__|
|
||||||
|
// \/ \/ \/ \//_____/ \/
|
||||||
|
|
||||||
|
// Manager represents a struct that contains session provider and its configuration.
|
||||||
|
type Manager struct { |
||||||
|
provider Provider |
||||||
|
opt Options |
||||||
|
} |
||||||
|
|
||||||
|
// NewManager creates and returns a new session manager by given provider name and configuration.
|
||||||
|
// It panics when given provider isn't registered.
|
||||||
|
func NewManager(name string, opt Options) (*Manager, error) { |
||||||
|
p, ok := providers[name] |
||||||
|
if !ok { |
||||||
|
return nil, fmt.Errorf("session: unknown provider '%s'(forgotten import?)", name) |
||||||
|
} |
||||||
|
return &Manager{p, opt}, p.Init(opt.Maxlifetime, opt.ProviderConfig) |
||||||
|
} |
||||||
|
|
||||||
|
// sessionID generates a new session ID with rand string, unix nano time, remote addr by hash function.
|
||||||
|
func (m *Manager) sessionID() string { |
||||||
|
return hex.EncodeToString(generateRandomKey(m.opt.IDLength / 2)) |
||||||
|
} |
||||||
|
|
||||||
|
// validSessionID tests whether a provided session ID is a valid session ID.
|
||||||
|
func (m *Manager) validSessionID(sid string) (bool, error) { |
||||||
|
if len(sid) != m.opt.IDLength { |
||||||
|
return false, fmt.Errorf("invalid 'sid': %s %d != %d", sid, len(sid), m.opt.IDLength) |
||||||
|
} |
||||||
|
|
||||||
|
for i := range sid { |
||||||
|
switch { |
||||||
|
case '0' <= sid[i] && sid[i] <= '9': |
||||||
|
case 'a' <= sid[i] && sid[i] <= 'f': |
||||||
|
default: |
||||||
|
return false, errors.New("invalid 'sid': " + sid) |
||||||
|
} |
||||||
|
} |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Start starts a session by generating new one
|
||||||
|
// or retrieve existence one by reading session ID from HTTP request if it's valid.
|
||||||
|
func (m *Manager) Start(resp http.ResponseWriter, req *http.Request) (RawStore, error) { |
||||||
|
sid := GetCookie(req, m.opt.CookieName) |
||||||
|
valid, _ := m.validSessionID(sid) |
||||||
|
if len(sid) > 0 && valid && m.provider.Exist(sid) { |
||||||
|
return m.provider.Read(sid) |
||||||
|
} |
||||||
|
|
||||||
|
sid = m.sessionID() |
||||||
|
sess, err := m.provider.Read(sid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
cookie := &http.Cookie{ |
||||||
|
Name: m.opt.CookieName, |
||||||
|
Value: sid, |
||||||
|
Path: m.opt.CookiePath, |
||||||
|
HttpOnly: true, |
||||||
|
Secure: m.opt.Secure, |
||||||
|
Domain: m.opt.Domain, |
||||||
|
SameSite: m.opt.SameSite, |
||||||
|
} |
||||||
|
if m.opt.CookieLifeTime >= 0 { |
||||||
|
cookie.MaxAge = m.opt.CookieLifeTime |
||||||
|
} |
||||||
|
http.SetCookie(resp, cookie) |
||||||
|
req.AddCookie(cookie) |
||||||
|
return sess, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Read returns raw session store by session ID.
|
||||||
|
func (m *Manager) Read(sid string) (RawStore, error) { |
||||||
|
// Ensure we're trying to read a valid session ID
|
||||||
|
if _, err := m.validSessionID(sid); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return m.provider.Read(sid) |
||||||
|
} |
||||||
|
|
||||||
|
// Destroy deletes a session by given ID.
|
||||||
|
func (m *Manager) Destroy(resp http.ResponseWriter, req *http.Request) error { |
||||||
|
sid := GetCookie(req, m.opt.CookieName) |
||||||
|
if len(sid) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := m.validSessionID(sid); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := m.provider.Destroy(sid); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
cookie := &http.Cookie{ |
||||||
|
Name: m.opt.CookieName, |
||||||
|
Path: m.opt.CookiePath, |
||||||
|
HttpOnly: true, |
||||||
|
Expires: time.Now(), |
||||||
|
MaxAge: -1, |
||||||
|
} |
||||||
|
http.SetCookie(resp, cookie) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// RegenerateID regenerates a session store from old session ID to new one.
|
||||||
|
func (m *Manager) RegenerateID(resp http.ResponseWriter, req *http.Request) (sess RawStore, err error) { |
||||||
|
sid := m.sessionID() |
||||||
|
oldsid := GetCookie(req, m.opt.CookieName) |
||||||
|
_, err = m.validSessionID(oldsid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
sess, err = m.provider.Regenerate(oldsid, sid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
cookie := &http.Cookie{ |
||||||
|
Name: m.opt.CookieName, |
||||||
|
Value: sid, |
||||||
|
Path: m.opt.CookiePath, |
||||||
|
HttpOnly: true, |
||||||
|
Secure: m.opt.Secure, |
||||||
|
Domain: m.opt.Domain, |
||||||
|
SameSite: m.opt.SameSite, |
||||||
|
} |
||||||
|
if m.opt.CookieLifeTime >= 0 { |
||||||
|
cookie.MaxAge = m.opt.CookieLifeTime |
||||||
|
} |
||||||
|
http.SetCookie(resp, cookie) |
||||||
|
req.AddCookie(cookie) |
||||||
|
return sess, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Count counts and returns number of sessions.
|
||||||
|
func (m *Manager) Count() int { |
||||||
|
return m.provider.Count() |
||||||
|
} |
||||||
|
|
||||||
|
// GC starts GC job in a certain period.
|
||||||
|
func (m *Manager) GC() { |
||||||
|
m.provider.GC() |
||||||
|
} |
||||||
|
|
||||||
|
// startGC starts GC job in a certain period.
|
||||||
|
func (m *Manager) startGC() { |
||||||
|
m.GC() |
||||||
|
time.AfterFunc(time.Duration(m.opt.Gclifetime)*time.Second, func() { m.startGC() }) |
||||||
|
} |
||||||
|
|
||||||
|
// SetSecure indicates whether to set cookie with HTTPS or not.
|
||||||
|
func (m *Manager) SetSecure(secure bool) { |
||||||
|
m.opt.Secure = secure |
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
// Copyright 2013 Beego Authors
|
||||||
|
// Copyright 2014 The Macaron Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package session |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/rand" |
||||||
|
"encoding/gob" |
||||||
|
"io" |
||||||
|
|
||||||
|
"github.com/unknwon/com" |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
gob.Register([]interface{}{}) |
||||||
|
gob.Register(map[int]interface{}{}) |
||||||
|
gob.Register(map[string]interface{}{}) |
||||||
|
gob.Register(map[interface{}]interface{}{}) |
||||||
|
gob.Register(map[string]string{}) |
||||||
|
gob.Register(map[int]string{}) |
||||||
|
gob.Register(map[int]int{}) |
||||||
|
gob.Register(map[int]int64{}) |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeGob encodes obj with gob
|
||||||
|
func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) { |
||||||
|
for _, v := range obj { |
||||||
|
gob.Register(v) |
||||||
|
} |
||||||
|
buf := bytes.NewBuffer(nil) |
||||||
|
err := gob.NewEncoder(buf).Encode(obj) |
||||||
|
return buf.Bytes(), err |
||||||
|
} |
||||||
|
|
||||||
|
// DecodeGob decodes bytes to obj
|
||||||
|
func DecodeGob(encoded []byte) (out map[interface{}]interface{}, err error) { |
||||||
|
buf := bytes.NewBuffer(encoded) |
||||||
|
err = gob.NewDecoder(buf).Decode(&out) |
||||||
|
return out, err |
||||||
|
} |
||||||
|
|
||||||
|
// NOTE: A local copy in case of underlying package change
|
||||||
|
var alphanum = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") |
||||||
|
|
||||||
|
// generateRandomKey creates a random key with the given strength.
|
||||||
|
func generateRandomKey(strength int) []byte { |
||||||
|
k := make([]byte, strength) |
||||||
|
if n, err := io.ReadFull(rand.Reader, k); n != strength || err != nil { |
||||||
|
return com.RandomCreateBytes(strength, alphanum...) |
||||||
|
} |
||||||
|
return k |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
module github.com/couchbase/gomemcached |
||||||
|
|
||||||
|
go 1.13 |
@ -1,20 +0,0 @@ |
|||||||
language: go |
|
||||||
|
|
||||||
go: |
|
||||||
- 1.10.x |
|
||||||
- 1.11.x |
|
||||||
- 1.12.x |
|
||||||
- 1.13.x |
|
||||||
- 1.14.x |
|
||||||
|
|
||||||
script: |
|
||||||
- go get -d -t ./... |
|
||||||
- go vet ./... |
|
||||||
- go test ./... |
|
||||||
- > |
|
||||||
go_version=$(go version); |
|
||||||
if [ ${go_version:13:4} = "1.12" ]; then |
|
||||||
go get -u golang.org/x/tools/cmd/goimports; |
|
||||||
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :; |
|
||||||
fi |
|
||||||
|
|
@ -0,0 +1,28 @@ |
|||||||
|
package middleware |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"path" |
||||||
|
|
||||||
|
"github.com/go-chi/chi" |
||||||
|
) |
||||||
|
|
||||||
|
// CleanPath middleware will clean out double slash mistakes from a user's request path.
|
||||||
|
// For example, if a user requests /users//1 or //users////1 will both be treated as: /users/1
|
||||||
|
func CleanPath(next http.Handler) http.Handler { |
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||||
|
rctx := chi.RouteContext(r.Context()) |
||||||
|
|
||||||
|
routePath := rctx.RoutePath |
||||||
|
if routePath == "" { |
||||||
|
if r.URL.RawPath != "" { |
||||||
|
routePath = r.URL.RawPath |
||||||
|
} else { |
||||||
|
routePath = r.URL.Path |
||||||
|
} |
||||||
|
rctx.RoutePath = path.Clean(routePath) |
||||||
|
} |
||||||
|
|
||||||
|
next.ServeHTTP(w, r) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects) |
||||||
|
*.o |
||||||
|
*.a |
||||||
|
*.so |
||||||
|
|
||||||
|
# Folders |
||||||
|
_obj |
||||||
|
_test |
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes |
||||||
|
*.[568vq] |
||||||
|
[568vq].out |
||||||
|
|
||||||
|
*.cgo1.go |
||||||
|
*.cgo2.c |
||||||
|
_cgo_defun.c |
||||||
|
_cgo_gotypes.go |
||||||
|
_cgo_export.* |
||||||
|
|
||||||
|
_testmain.go |
||||||
|
|
||||||
|
*.exe |
||||||
|
*.test |
||||||
|
|
||||||
|
|
||||||
|
*.pem |
||||||
|
.DS_Store |
@ -0,0 +1,15 @@ |
|||||||
|
language: go |
||||||
|
|
||||||
|
go: |
||||||
|
- 1.11.x |
||||||
|
- 1.12.x |
||||||
|
- tip |
||||||
|
|
||||||
|
env: |
||||||
|
- GO111MODULE=on |
||||||
|
|
||||||
|
install: |
||||||
|
- go mod download |
||||||
|
|
||||||
|
script: |
||||||
|
- go test -v -race -tags=integration |
@ -0,0 +1,20 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2014 Cory Jacobsen |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||||
|
this software and associated documentation files (the "Software"), to deal in |
||||||
|
the Software without restriction, including without limitation the rights to |
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so, |
||||||
|
subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,508 @@ |
|||||||
|
# Render [![GoDoc](http://godoc.org/github.com/unrolled/render?status.svg)](http://godoc.org/github.com/unrolled/render) [![Build Status](https://travis-ci.org/unrolled/render.svg)](https://travis-ci.org/unrolled/render) |
||||||
|
|
||||||
|
Render is a package that provides functionality for easily rendering JSON, XML, text, binary data, and HTML templates. This package is based on the [Martini](https://github.com/go-martini/martini) [render](https://github.com/martini-contrib/render) work. |
||||||
|
|
||||||
|
## Block Deprecation Notice |
||||||
|
Go 1.6 introduces a new [block](https://github.com/golang/go/blob/release-branch.go1.6/src/html/template/example_test.go#L128) action. This conflicts with Render's included `block` template function. To provide an easy migration path, a new function was created called `partial`. It is a duplicate of the old `block` function. It is advised that all users of the `block` function update their code to avoid any issues in the future. Previous to Go 1.6, Render's `block` functionality will continue to work but a message will be logged urging you to migrate to the new `partial` function. |
||||||
|
|
||||||
|
## Usage |
||||||
|
Render can be used with pretty much any web framework providing you can access the `http.ResponseWriter` from your handler. The rendering functions simply wraps Go's existing functionality for marshaling and rendering data. |
||||||
|
|
||||||
|
- HTML: Uses the [html/template](http://golang.org/pkg/html/template/) package to render HTML templates. |
||||||
|
- JSON: Uses the [encoding/json](http://golang.org/pkg/encoding/json/) package to marshal data into a JSON-encoded response. |
||||||
|
- XML: Uses the [encoding/xml](http://golang.org/pkg/encoding/xml/) package to marshal data into an XML-encoded response. |
||||||
|
- Binary data: Passes the incoming data straight through to the `http.ResponseWriter`. |
||||||
|
- Text: Passes the incoming string straight through to the `http.ResponseWriter`. |
||||||
|
|
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/xml" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
type ExampleXml struct { |
||||||
|
XMLName xml.Name `xml:"example"` |
||||||
|
One string `xml:"one,attr"` |
||||||
|
Two string `xml:"two,attr"` |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New() |
||||||
|
mux := http.NewServeMux() |
||||||
|
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
w.Write([]byte("Welcome, visit sub pages now.")) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Data(w, http.StatusOK, []byte("Some binary data here.")) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/text", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Text(w, http.StatusOK, "Plain text here") |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/json", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSON(w, http.StatusOK, map[string]string{"hello": "json"}) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/jsonp", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSONP(w, http.StatusOK, "callbackName", map[string]string{"hello": "jsonp"}) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/xml", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.XML(w, http.StatusOK, ExampleXml{One: "hello", Two: "xml"}) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/html", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Assumes you have a template in ./templates called "example.tmpl" |
||||||
|
// $ mkdir -p templates && echo "<h1>Hello {{.}}.</h1>" > templates/example.tmpl |
||||||
|
r.HTML(w, http.StatusOK, "example", "World") |
||||||
|
}) |
||||||
|
|
||||||
|
http.ListenAndServe("127.0.0.1:3000", mux) |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
~~~ html |
||||||
|
<!-- templates/example.tmpl --> |
||||||
|
<h1>Hello {{.}}.</h1> |
||||||
|
~~~ |
||||||
|
|
||||||
|
### Available Options |
||||||
|
Render comes with a variety of configuration options _(Note: these are not the default option values. See the defaults below.)_: |
||||||
|
|
||||||
|
~~~ go |
||||||
|
// ... |
||||||
|
r := render.New(render.Options{ |
||||||
|
Directory: "templates", // Specify what path to load the templates from. |
||||||
|
FileSystem: &LocalFileSystem{}, // Specify filesystem from where files are loaded. |
||||||
|
Asset: func(name string) ([]byte, error) { // Load from an Asset function instead of file. |
||||||
|
return []byte("template content"), nil |
||||||
|
}, |
||||||
|
AssetNames: func() []string { // Return a list of asset names for the Asset function |
||||||
|
return []string{"filename.tmpl"} |
||||||
|
}, |
||||||
|
Layout: "layout", // Specify a layout template. Layouts can call {{ yield }} to render the current template or {{ partial "css" }} to render a partial from the current template. |
||||||
|
Extensions: []string{".tmpl", ".html"}, // Specify extensions to load for templates. |
||||||
|
Funcs: []template.FuncMap{AppHelpers}, // Specify helper function maps for templates to access. |
||||||
|
Delims: render.Delims{"{[{", "}]}"}, // Sets delimiters to the specified strings. |
||||||
|
Charset: "UTF-8", // Sets encoding for content-types. Default is "UTF-8". |
||||||
|
DisableCharset: true, // Prevents the charset from being appended to the content type header. |
||||||
|
IndentJSON: true, // Output human readable JSON. |
||||||
|
IndentXML: true, // Output human readable XML. |
||||||
|
PrefixJSON: []byte(")]}',\n"), // Prefixes JSON responses with the given bytes. |
||||||
|
PrefixXML: []byte("<?xml version='1.0' encoding='UTF-8'?>"), // Prefixes XML responses with the given bytes. |
||||||
|
HTMLContentType: "application/xhtml+xml", // Output XHTML content type instead of default "text/html". |
||||||
|
IsDevelopment: true, // Render will now recompile the templates on every HTML response. |
||||||
|
UnEscapeHTML: true, // Replace ensure '&<>' are output correctly (JSON only). |
||||||
|
StreamingJSON: true, // Streams the JSON response via json.Encoder. |
||||||
|
RequirePartials: true, // Return an error if a template is missing a partial used in a layout. |
||||||
|
DisableHTTPErrorRendering: true, // Disables automatic rendering of http.StatusInternalServerError when an error occurs. |
||||||
|
}) |
||||||
|
// ... |
||||||
|
~~~ |
||||||
|
|
||||||
|
### Default Options |
||||||
|
These are the preset options for Render: |
||||||
|
|
||||||
|
~~~ go |
||||||
|
r := render.New() |
||||||
|
|
||||||
|
// Is the same as the default configuration options: |
||||||
|
|
||||||
|
r := render.New(render.Options{ |
||||||
|
Directory: "templates", |
||||||
|
FileSystem: &LocalFileSystem{}, |
||||||
|
Asset: nil, |
||||||
|
AssetNames: nil, |
||||||
|
Layout: "", |
||||||
|
Extensions: []string{".tmpl"}, |
||||||
|
Funcs: []template.FuncMap{}, |
||||||
|
Delims: render.Delims{"{{", "}}"}, |
||||||
|
Charset: "UTF-8", |
||||||
|
DisableCharset: false, |
||||||
|
IndentJSON: false, |
||||||
|
IndentXML: false, |
||||||
|
PrefixJSON: []byte(""), |
||||||
|
PrefixXML: []byte(""), |
||||||
|
BinaryContentType: "application/octet-stream", |
||||||
|
HTMLContentType: "text/html", |
||||||
|
JSONContentType: "application/json", |
||||||
|
JSONPContentType: "application/javascript", |
||||||
|
TextContentType: "text/plain", |
||||||
|
XMLContentType: "application/xhtml+xml", |
||||||
|
IsDevelopment: false, |
||||||
|
UnEscapeHTML: false, |
||||||
|
StreamingJSON: false, |
||||||
|
RequirePartials: false, |
||||||
|
DisableHTTPErrorRendering: false, |
||||||
|
}) |
||||||
|
~~~ |
||||||
|
|
||||||
|
### JSON vs Streaming JSON |
||||||
|
By default, Render does **not** stream JSON to the `http.ResponseWriter`. It instead marshalls your object into a byte array, and if no errors occurred, writes that byte array to the `http.ResponseWriter`. If you would like to use the built it in streaming functionality (`json.Encoder`), you can set the `StreamingJSON` setting to `true`. This will stream the output directly to the `http.ResponseWriter`. Also note that streaming is only implemented in `render.JSON` and not `render.JSONP`, and the `UnEscapeHTML` and `Indent` options are ignored when streaming. |
||||||
|
|
||||||
|
### Loading Templates |
||||||
|
By default Render will attempt to load templates with a '.tmpl' extension from the "templates" directory. Templates are found by traversing the templates directory and are named by path and basename. For instance, the following directory structure: |
||||||
|
|
||||||
|
~~~ |
||||||
|
templates/ |
||||||
|
| |
||||||
|
|__ admin/ |
||||||
|
| | |
||||||
|
| |__ index.tmpl |
||||||
|
| | |
||||||
|
| |__ edit.tmpl |
||||||
|
| |
||||||
|
|__ home.tmpl |
||||||
|
~~~ |
||||||
|
|
||||||
|
Will provide the following templates: |
||||||
|
~~~ |
||||||
|
admin/index |
||||||
|
admin/edit |
||||||
|
home |
||||||
|
~~~ |
||||||
|
|
||||||
|
You can also load templates from memory by providing the Asset and AssetNames options, |
||||||
|
e.g. when generating an asset file using [go-bindata](https://github.com/jteeuwen/go-bindata). |
||||||
|
|
||||||
|
### Layouts |
||||||
|
Render provides `yield` and `partial` functions for layouts to access: |
||||||
|
~~~ go |
||||||
|
// ... |
||||||
|
r := render.New(render.Options{ |
||||||
|
Layout: "layout", |
||||||
|
}) |
||||||
|
// ... |
||||||
|
~~~ |
||||||
|
|
||||||
|
~~~ html |
||||||
|
<!-- templates/layout.tmpl --> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>My Layout</title> |
||||||
|
<!-- Render the partial template called `css-$current_template` here --> |
||||||
|
{{ partial "css" }} |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<!-- render the partial template called `header-$current_template` here --> |
||||||
|
{{ partial "header" }} |
||||||
|
<!-- Render the current template here --> |
||||||
|
{{ yield }} |
||||||
|
<!-- render the partial template called `footer-$current_template` here --> |
||||||
|
{{ partial "footer" }} |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
~~~ |
||||||
|
|
||||||
|
`current` can also be called to get the current template being rendered. |
||||||
|
~~~ html |
||||||
|
<!-- templates/layout.tmpl --> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>My Layout</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
This is the {{ current }} page. |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
~~~ |
||||||
|
|
||||||
|
Partials are defined by individual templates as seen below. The partial template's |
||||||
|
name needs to be defined as "{partial name}-{template name}". |
||||||
|
~~~ html |
||||||
|
<!-- templates/home.tmpl --> |
||||||
|
{{ define "header-home" }} |
||||||
|
<h1>Home</h1> |
||||||
|
{{ end }} |
||||||
|
|
||||||
|
{{ define "footer-home"}} |
||||||
|
<p>The End</p> |
||||||
|
{{ end }} |
||||||
|
~~~ |
||||||
|
|
||||||
|
By default, the template is not required to define all partials referenced in the |
||||||
|
layout. If you want an error to be returned when a template does not define a |
||||||
|
partial, set `Options.RequirePartials = true`. |
||||||
|
|
||||||
|
### Character Encodings |
||||||
|
Render will automatically set the proper Content-Type header based on which function you call. See below for an example of what the default settings would output (note that UTF-8 is the default, and binary data does not output the charset): |
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/xml" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
type ExampleXml struct { |
||||||
|
XMLName xml.Name `xml:"example"` |
||||||
|
One string `xml:"one,attr"` |
||||||
|
Two string `xml:"two,attr"` |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New(render.Options{}) |
||||||
|
mux := http.NewServeMux() |
||||||
|
|
||||||
|
// This will set the Content-Type header to "application/octet-stream". |
||||||
|
// Note that this does not receive a charset value. |
||||||
|
mux.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Data(w, http.StatusOK, []byte("Some binary data here.")) |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "application/json; charset=UTF-8". |
||||||
|
mux.HandleFunc("/json", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSON(w, http.StatusOK, map[string]string{"hello": "json"}) |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "text/xml; charset=UTF-8". |
||||||
|
mux.HandleFunc("/xml", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.XML(w, http.StatusOK, ExampleXml{One: "hello", Two: "xml"}) |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "text/plain; charset=UTF-8". |
||||||
|
mux.HandleFunc("/text", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Text(w, http.StatusOK, "Plain text here") |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "text/html; charset=UTF-8". |
||||||
|
mux.HandleFunc("/html", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Assumes you have a template in ./templates called "example.tmpl" |
||||||
|
// $ mkdir -p templates && echo "<h1>Hello {{.}}.</h1>" > templates/example.tmpl |
||||||
|
r.HTML(w, http.StatusOK, "example", "World") |
||||||
|
}) |
||||||
|
|
||||||
|
http.ListenAndServe("127.0.0.1:3000", mux) |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
In order to change the charset, you can set the `Charset` within the `render.Options` to your encoding value: |
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/xml" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
type ExampleXml struct { |
||||||
|
XMLName xml.Name `xml:"example"` |
||||||
|
One string `xml:"one,attr"` |
||||||
|
Two string `xml:"two,attr"` |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New(render.Options{ |
||||||
|
Charset: "ISO-8859-1", |
||||||
|
}) |
||||||
|
mux := http.NewServeMux() |
||||||
|
|
||||||
|
// This will set the Content-Type header to "application/octet-stream". |
||||||
|
// Note that this does not receive a charset value. |
||||||
|
mux.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Data(w, http.StatusOK, []byte("Some binary data here.")) |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "application/json; charset=ISO-8859-1". |
||||||
|
mux.HandleFunc("/json", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSON(w, http.StatusOK, map[string]string{"hello": "json"}) |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "text/xml; charset=ISO-8859-1". |
||||||
|
mux.HandleFunc("/xml", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.XML(w, http.StatusOK, ExampleXml{One: "hello", Two: "xml"}) |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "text/plain; charset=ISO-8859-1". |
||||||
|
mux.HandleFunc("/text", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Text(w, http.StatusOK, "Plain text here") |
||||||
|
}) |
||||||
|
|
||||||
|
// This will set the Content-Type header to "text/html; charset=ISO-8859-1". |
||||||
|
mux.HandleFunc("/html", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Assumes you have a template in ./templates called "example.tmpl" |
||||||
|
// $ mkdir -p templates && echo "<h1>Hello {{.}}.</h1>" > templates/example.tmpl |
||||||
|
r.HTML(w, http.StatusOK, "example", "World") |
||||||
|
}) |
||||||
|
|
||||||
|
http.ListenAndServe("127.0.0.1:3000", mux) |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
### Error Handling |
||||||
|
|
||||||
|
The rendering functions return any errors from the rendering engine. |
||||||
|
By default, they will also write the error to the HTTP response and set the status code to 500. You can disable |
||||||
|
this behavior so that you can handle errors yourself by setting |
||||||
|
`Options.DisableHTTPErrorRendering: true`. |
||||||
|
|
||||||
|
~~~go |
||||||
|
r := render.New(render.Options{ |
||||||
|
DisableHTTPErrorRendering: true, |
||||||
|
}) |
||||||
|
|
||||||
|
//... |
||||||
|
|
||||||
|
err := r.HTML(w, http.StatusOK, "example", "World") |
||||||
|
if err != nil{ |
||||||
|
http.Redirect(w, r, "/my-custom-500", http.StatusFound) |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
## Integration Examples |
||||||
|
|
||||||
|
### [Echo](https://github.com/labstack/echo) |
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/labstack/echo" |
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
type RenderWrapper struct { // We need to wrap the renderer because we need a different signature for echo. |
||||||
|
rnd *render.Render |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RenderWrapper) Render(w io.Writer, name string, data interface{},c echo.Context) error { |
||||||
|
return r.rnd.HTML(w, 0, name, data) // The zero status code is overwritten by echo. |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := &RenderWrapper{render.New()} |
||||||
|
|
||||||
|
e := echo.New() |
||||||
|
|
||||||
|
e.Renderer = r |
||||||
|
|
||||||
|
e.GET("/", func(c echo.Context) error { |
||||||
|
return c.Render(http.StatusOK, "TemplateName", "TemplateData") |
||||||
|
}) |
||||||
|
|
||||||
|
e.Logger.Fatal(e.Start(":1323")) |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
### [Gin](https://github.com/gin-gonic/gin) |
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/gin-gonic/gin" |
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New(render.Options{ |
||||||
|
IndentJSON: true, |
||||||
|
}) |
||||||
|
|
||||||
|
router := gin.Default() |
||||||
|
|
||||||
|
router.GET("/", func(c *gin.Context) { |
||||||
|
r.JSON(c.Writer, http.StatusOK, map[string]string{"welcome": "This is rendered JSON!"}) |
||||||
|
}) |
||||||
|
|
||||||
|
router.Run(":3000") |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
### [Goji](https://github.com/zenazn/goji) |
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/zenazn/goji" |
||||||
|
"github.com/zenazn/goji/web" |
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New(render.Options{ |
||||||
|
IndentJSON: true, |
||||||
|
}) |
||||||
|
|
||||||
|
goji.Get("/", func(c web.C, w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSON(w, http.StatusOK, map[string]string{"welcome": "This is rendered JSON!"}) |
||||||
|
}) |
||||||
|
goji.Serve() // Defaults to ":8000". |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
### [Negroni](https://github.com/codegangsta/negroni) |
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/urfave/negroni" |
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New(render.Options{ |
||||||
|
IndentJSON: true, |
||||||
|
}) |
||||||
|
mux := http.NewServeMux() |
||||||
|
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSON(w, http.StatusOK, map[string]string{"welcome": "This is rendered JSON!"}) |
||||||
|
}) |
||||||
|
|
||||||
|
n := negroni.Classic() |
||||||
|
n.UseHandler(mux) |
||||||
|
n.Run(":3000") |
||||||
|
} |
||||||
|
~~~ |
||||||
|
|
||||||
|
### [Traffic](https://github.com/pilu/traffic) |
||||||
|
~~~ go |
||||||
|
// main.go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/pilu/traffic" |
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New(render.Options{ |
||||||
|
IndentJSON: true, |
||||||
|
}) |
||||||
|
|
||||||
|
router := traffic.New() |
||||||
|
router.Get("/", func(w traffic.ResponseWriter, req *traffic.Request) { |
||||||
|
r.JSON(w, http.StatusOK, map[string]string{"welcome": "This is rendered JSON!"}) |
||||||
|
}) |
||||||
|
|
||||||
|
router.Run() |
||||||
|
} |
||||||
|
~~~ |
@ -0,0 +1,46 @@ |
|||||||
|
package render |
||||||
|
|
||||||
|
import "bytes" |
||||||
|
|
||||||
|
// bufPool represents a reusable buffer pool for executing templates into.
|
||||||
|
var bufPool *BufferPool |
||||||
|
|
||||||
|
// BufferPool implements a pool of bytes.Buffers in the form of a bounded channel.
|
||||||
|
// Pulled from the github.com/oxtoacart/bpool package (Apache licensed).
|
||||||
|
type BufferPool struct { |
||||||
|
c chan *bytes.Buffer |
||||||
|
} |
||||||
|
|
||||||
|
// NewBufferPool creates a new BufferPool bounded to the given size.
|
||||||
|
func NewBufferPool(size int) (bp *BufferPool) { |
||||||
|
return &BufferPool{ |
||||||
|
c: make(chan *bytes.Buffer, size), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Get gets a Buffer from the BufferPool, or creates a new one if none are
|
||||||
|
// available in the pool.
|
||||||
|
func (bp *BufferPool) Get() (b *bytes.Buffer) { |
||||||
|
select { |
||||||
|
case b = <-bp.c: |
||||||
|
// reuse existing buffer
|
||||||
|
default: |
||||||
|
// create new buffer
|
||||||
|
b = bytes.NewBuffer([]byte{}) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Put returns the given Buffer to the BufferPool.
|
||||||
|
func (bp *BufferPool) Put(b *bytes.Buffer) { |
||||||
|
b.Reset() |
||||||
|
select { |
||||||
|
case bp.c <- b: |
||||||
|
default: // Discard the buffer if the pool is full.
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Initialize buffer pool for writing templates into.
|
||||||
|
func init() { |
||||||
|
bufPool = NewBufferPool(64) |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
/*Package render is a package that provides functionality for easily rendering JSON, XML, binary data, and HTML templates. |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/xml" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/unrolled/render" // or "gopkg.in/unrolled/render.v1"
|
||||||
|
) |
||||||
|
|
||||||
|
type ExampleXml struct { |
||||||
|
XMLName xml.Name `xml:"example"` |
||||||
|
One string `xml:"one,attr"` |
||||||
|
Two string `xml:"two,attr"` |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
r := render.New() |
||||||
|
mux := http.NewServeMux() |
||||||
|
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
w.Write([]byte("Welcome, visit sub pages now.")) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Data(w, http.StatusOK, []byte("Some binary data here.")) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/text", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.Text(w, http.StatusOK, "Plain text here") |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/json", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSON(w, http.StatusOK, map[string]string{"hello": "json"}) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/jsonp", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.JSONP(w, http.StatusOK, "callbackName", map[string]string{"hello": "jsonp"}) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/xml", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
r.XML(w, http.StatusOK, ExampleXml{One: "hello", Two: "xml"}) |
||||||
|
}) |
||||||
|
|
||||||
|
mux.HandleFunc("/html", func(w http.ResponseWriter, req *http.Request) { |
||||||
|
// Assumes you have a template in ./templates called "example.tmpl".
|
||||||
|
// $ mkdir -p templates && echo "<h1>Hello HTML world.</h1>" > templates/example.tmpl
|
||||||
|
r.HTML(w, http.StatusOK, "example", nil) |
||||||
|
}) |
||||||
|
|
||||||
|
http.ListenAndServe("0.0.0.0:3000", mux) |
||||||
|
} |
||||||
|
*/ |
||||||
|
package render |
@ -0,0 +1,217 @@ |
|||||||
|
package render |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"encoding/xml" |
||||||
|
"html/template" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
// Engine is the generic interface for all responses.
|
||||||
|
type Engine interface { |
||||||
|
Render(io.Writer, interface{}) error |
||||||
|
} |
||||||
|
|
||||||
|
// Head defines the basic ContentType and Status fields.
|
||||||
|
type Head struct { |
||||||
|
ContentType string |
||||||
|
Status int |
||||||
|
} |
||||||
|
|
||||||
|
// Data built-in renderer.
|
||||||
|
type Data struct { |
||||||
|
Head |
||||||
|
} |
||||||
|
|
||||||
|
// HTML built-in renderer.
|
||||||
|
type HTML struct { |
||||||
|
Head |
||||||
|
Name string |
||||||
|
Templates *template.Template |
||||||
|
} |
||||||
|
|
||||||
|
// JSON built-in renderer.
|
||||||
|
type JSON struct { |
||||||
|
Head |
||||||
|
Indent bool |
||||||
|
UnEscapeHTML bool |
||||||
|
Prefix []byte |
||||||
|
StreamingJSON bool |
||||||
|
} |
||||||
|
|
||||||
|
// JSONP built-in renderer.
|
||||||
|
type JSONP struct { |
||||||
|
Head |
||||||
|
Indent bool |
||||||
|
Callback string |
||||||
|
} |
||||||
|
|
||||||
|
// Text built-in renderer.
|
||||||
|
type Text struct { |
||||||
|
Head |
||||||
|
} |
||||||
|
|
||||||
|
// XML built-in renderer.
|
||||||
|
type XML struct { |
||||||
|
Head |
||||||
|
Indent bool |
||||||
|
Prefix []byte |
||||||
|
} |
||||||
|
|
||||||
|
// Write outputs the header content.
|
||||||
|
func (h Head) Write(w http.ResponseWriter) { |
||||||
|
w.Header().Set(ContentType, h.ContentType) |
||||||
|
w.WriteHeader(h.Status) |
||||||
|
} |
||||||
|
|
||||||
|
// Render a data response.
|
||||||
|
func (d Data) Render(w io.Writer, v interface{}) error { |
||||||
|
if hw, ok := w.(http.ResponseWriter); ok { |
||||||
|
c := hw.Header().Get(ContentType) |
||||||
|
if c != "" { |
||||||
|
d.Head.ContentType = c |
||||||
|
} |
||||||
|
d.Head.Write(hw) |
||||||
|
} |
||||||
|
|
||||||
|
w.Write(v.([]byte)) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Render a HTML response.
|
||||||
|
func (h HTML) Render(w io.Writer, binding interface{}) error { |
||||||
|
// Retrieve a buffer from the pool to write to.
|
||||||
|
out := bufPool.Get() |
||||||
|
err := h.Templates.ExecuteTemplate(out, h.Name, binding) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if hw, ok := w.(http.ResponseWriter); ok { |
||||||
|
h.Head.Write(hw) |
||||||
|
} |
||||||
|
out.WriteTo(w) |
||||||
|
|
||||||
|
// Return the buffer to the pool.
|
||||||
|
bufPool.Put(out) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Render a JSON response.
|
||||||
|
func (j JSON) Render(w io.Writer, v interface{}) error { |
||||||
|
if j.StreamingJSON { |
||||||
|
return j.renderStreamingJSON(w, v) |
||||||
|
} |
||||||
|
|
||||||
|
var result []byte |
||||||
|
var err error |
||||||
|
|
||||||
|
if j.Indent { |
||||||
|
result, err = json.MarshalIndent(v, "", " ") |
||||||
|
result = append(result, '\n') |
||||||
|
} else { |
||||||
|
result, err = json.Marshal(v) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Unescape HTML if needed.
|
||||||
|
if j.UnEscapeHTML { |
||||||
|
result = bytes.Replace(result, []byte("\\u003c"), []byte("<"), -1) |
||||||
|
result = bytes.Replace(result, []byte("\\u003e"), []byte(">"), -1) |
||||||
|
result = bytes.Replace(result, []byte("\\u0026"), []byte("&"), -1) |
||||||
|
} |
||||||
|
|
||||||
|
// JSON marshaled fine, write out the result.
|
||||||
|
if hw, ok := w.(http.ResponseWriter); ok { |
||||||
|
j.Head.Write(hw) |
||||||
|
} |
||||||
|
if len(j.Prefix) > 0 { |
||||||
|
w.Write(j.Prefix) |
||||||
|
} |
||||||
|
w.Write(result) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (j JSON) renderStreamingJSON(w io.Writer, v interface{}) error { |
||||||
|
if hw, ok := w.(http.ResponseWriter); ok { |
||||||
|
j.Head.Write(hw) |
||||||
|
} |
||||||
|
if len(j.Prefix) > 0 { |
||||||
|
w.Write(j.Prefix) |
||||||
|
} |
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(v) |
||||||
|
} |
||||||
|
|
||||||
|
// Render a JSONP response.
|
||||||
|
func (j JSONP) Render(w io.Writer, v interface{}) error { |
||||||
|
var result []byte |
||||||
|
var err error |
||||||
|
|
||||||
|
if j.Indent { |
||||||
|
result, err = json.MarshalIndent(v, "", " ") |
||||||
|
} else { |
||||||
|
result, err = json.Marshal(v) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// JSON marshaled fine, write out the result.
|
||||||
|
if hw, ok := w.(http.ResponseWriter); ok { |
||||||
|
j.Head.Write(hw) |
||||||
|
} |
||||||
|
w.Write([]byte(j.Callback + "(")) |
||||||
|
w.Write(result) |
||||||
|
w.Write([]byte(");")) |
||||||
|
|
||||||
|
// If indenting, append a new line.
|
||||||
|
if j.Indent { |
||||||
|
w.Write([]byte("\n")) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Render a text response.
|
||||||
|
func (t Text) Render(w io.Writer, v interface{}) error { |
||||||
|
if hw, ok := w.(http.ResponseWriter); ok { |
||||||
|
c := hw.Header().Get(ContentType) |
||||||
|
if c != "" { |
||||||
|
t.Head.ContentType = c |
||||||
|
} |
||||||
|
t.Head.Write(hw) |
||||||
|
} |
||||||
|
|
||||||
|
w.Write([]byte(v.(string))) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Render an XML response.
|
||||||
|
func (x XML) Render(w io.Writer, v interface{}) error { |
||||||
|
var result []byte |
||||||
|
var err error |
||||||
|
|
||||||
|
if x.Indent { |
||||||
|
result, err = xml.MarshalIndent(v, "", " ") |
||||||
|
result = append(result, '\n') |
||||||
|
} else { |
||||||
|
result, err = xml.Marshal(v) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// XML marshaled fine, write out the result.
|
||||||
|
if hw, ok := w.(http.ResponseWriter); ok { |
||||||
|
x.Head.Write(hw) |
||||||
|
} |
||||||
|
if len(x.Prefix) > 0 { |
||||||
|
w.Write(x.Prefix) |
||||||
|
} |
||||||
|
w.Write(result) |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package render |
||||||
|
|
||||||
|
import ( |
||||||
|
"io/ioutil" |
||||||
|
"path/filepath" |
||||||
|
) |
||||||
|
|
||||||
|
type FileSystem interface { |
||||||
|
Walk(root string, walkFn filepath.WalkFunc) error |
||||||
|
ReadFile(filename string) ([]byte, error) |
||||||
|
} |
||||||
|
|
||||||
|
type LocalFileSystem struct{} |
||||||
|
|
||||||
|
func (LocalFileSystem) Walk(root string, walkFn filepath.WalkFunc) error { |
||||||
|
return filepath.Walk(root, walkFn) |
||||||
|
} |
||||||
|
|
||||||
|
func (LocalFileSystem) ReadFile(filename string) ([]byte, error) { |
||||||
|
return ioutil.ReadFile(filename) |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
module github.com/unrolled/render |
||||||
|
|
||||||
|
go 1.12 |
||||||
|
|
||||||
|
require github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 |
@ -0,0 +1,2 @@ |
|||||||
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= |
||||||
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= |
@ -0,0 +1,21 @@ |
|||||||
|
// +build go1.6
|
||||||
|
|
||||||
|
package render |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"html/template" |
||||||
|
) |
||||||
|
|
||||||
|
// Included helper functions for use when rendering HTML.
|
||||||
|
var helperFuncs = template.FuncMap{ |
||||||
|
"yield": func() (string, error) { |
||||||
|
return "", fmt.Errorf("yield called with no layout defined") |
||||||
|
}, |
||||||
|
"partial": func() (string, error) { |
||||||
|
return "", fmt.Errorf("block called with no layout defined") |
||||||
|
}, |
||||||
|
"current": func() (string, error) { |
||||||
|
return "", nil |
||||||
|
}, |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
// +build !go1.6
|
||||||
|
|
||||||
|
package render |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"html/template" |
||||||
|
) |
||||||
|
|
||||||
|
// Included helper functions for use when rendering HTML.
|
||||||
|
var helperFuncs = template.FuncMap{ |
||||||
|
"yield": func() (string, error) { |
||||||
|
return "", fmt.Errorf("yield called with no layout defined") |
||||||
|
}, |
||||||
|
// `block` is deprecated! Use the `partial` below if you need this functionality still.
|
||||||
|
// Otherwise, checkout Go's `block` implementation introduced in 1.6
|
||||||
|
"block": func() (string, error) { |
||||||
|
return "", fmt.Errorf("block called with no layout defined") |
||||||
|
}, |
||||||
|
"partial": func() (string, error) { |
||||||
|
return "", fmt.Errorf("block called with no layout defined") |
||||||
|
}, |
||||||
|
"current": func() (string, error) { |
||||||
|
return "", nil |
||||||
|
}, |
||||||
|
} |
@ -0,0 +1,480 @@ |
|||||||
|
package render |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"html/template" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// ContentBinary header value for binary data.
|
||||||
|
ContentBinary = "application/octet-stream" |
||||||
|
// ContentHTML header value for HTML data.
|
||||||
|
ContentHTML = "text/html" |
||||||
|
// ContentJSON header value for JSON data.
|
||||||
|
ContentJSON = "application/json" |
||||||
|
// ContentJSONP header value for JSONP data.
|
||||||
|
ContentJSONP = "application/javascript" |
||||||
|
// ContentLength header constant.
|
||||||
|
ContentLength = "Content-Length" |
||||||
|
// ContentText header value for Text data.
|
||||||
|
ContentText = "text/plain" |
||||||
|
// ContentType header constant.
|
||||||
|
ContentType = "Content-Type" |
||||||
|
// ContentXHTML header value for XHTML data.
|
||||||
|
ContentXHTML = "application/xhtml+xml" |
||||||
|
// ContentXML header value for XML data.
|
||||||
|
ContentXML = "text/xml" |
||||||
|
// Default character encoding.
|
||||||
|
defaultCharset = "UTF-8" |
||||||
|
) |
||||||
|
|
||||||
|
// helperFuncs had to be moved out. See helpers.go|helpers_pre16.go files.
|
||||||
|
|
||||||
|
// Delims represents a set of Left and Right delimiters for HTML template rendering.
|
||||||
|
type Delims struct { |
||||||
|
// Left delimiter, defaults to {{.
|
||||||
|
Left string |
||||||
|
// Right delimiter, defaults to }}.
|
||||||
|
Right string |
||||||
|
} |
||||||
|
|
||||||
|
// Options is a struct for specifying configuration options for the render.Render object.
|
||||||
|
type Options struct { |
||||||
|
// Directory to load templates. Default is "templates".
|
||||||
|
Directory string |
||||||
|
// FileSystem to access files
|
||||||
|
FileSystem FileSystem |
||||||
|
// Asset function to use in place of directory. Defaults to nil.
|
||||||
|
Asset func(name string) ([]byte, error) |
||||||
|
// AssetNames function to use in place of directory. Defaults to nil.
|
||||||
|
AssetNames func() []string |
||||||
|
// Layout template name. Will not render a layout if blank (""). Defaults to blank ("").
|
||||||
|
Layout string |
||||||
|
// Extensions to parse template files from. Defaults to [".tmpl"].
|
||||||
|
Extensions []string |
||||||
|
// Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Defaults to empty map.
|
||||||
|
Funcs []template.FuncMap |
||||||
|
// Delims sets the action delimiters to the specified strings in the Delims struct.
|
||||||
|
Delims Delims |
||||||
|
// Appends the given character set to the Content-Type header. Default is "UTF-8".
|
||||||
|
Charset string |
||||||
|
// If DisableCharset is set to true, it will not append the above Charset value to the Content-Type header. Default is false.
|
||||||
|
DisableCharset bool |
||||||
|
// Outputs human readable JSON.
|
||||||
|
IndentJSON bool |
||||||
|
// Outputs human readable XML. Default is false.
|
||||||
|
IndentXML bool |
||||||
|
// Prefixes the JSON output with the given bytes. Default is false.
|
||||||
|
PrefixJSON []byte |
||||||
|
// Prefixes the XML output with the given bytes.
|
||||||
|
PrefixXML []byte |
||||||
|
// Allows changing the binary content type.
|
||||||
|
BinaryContentType string |
||||||
|
// Allows changing the HTML content type.
|
||||||
|
HTMLContentType string |
||||||
|
// Allows changing the JSON content type.
|
||||||
|
JSONContentType string |
||||||
|
// Allows changing the JSONP content type.
|
||||||
|
JSONPContentType string |
||||||
|
// Allows changing the Text content type.
|
||||||
|
TextContentType string |
||||||
|
// Allows changing the XML content type.
|
||||||
|
XMLContentType string |
||||||
|
// If IsDevelopment is set to true, this will recompile the templates on every request. Default is false.
|
||||||
|
IsDevelopment bool |
||||||
|
// Unescape HTML characters "&<>" to their original values. Default is false.
|
||||||
|
UnEscapeHTML bool |
||||||
|
// Streams JSON responses instead of marshalling prior to sending. Default is false.
|
||||||
|
StreamingJSON bool |
||||||
|
// Require that all partials executed in the layout are implemented in all templates using the layout. Default is false.
|
||||||
|
RequirePartials bool |
||||||
|
// Deprecated: Use the above `RequirePartials` instead of this. As of Go 1.6, blocks are built in. Default is false.
|
||||||
|
RequireBlocks bool |
||||||
|
// Disables automatic rendering of http.StatusInternalServerError when an error occurs. Default is false.
|
||||||
|
DisableHTTPErrorRendering bool |
||||||
|
// Enables using partials without the current filename suffix which allows use of the same template in multiple files. e.g {{ partial "carosuel" }} inside the home template will match carosel-home or carosel.
|
||||||
|
// ***NOTE*** - This option should be named RenderPartialsWithoutSuffix as that is what it does. "Prefix" is a typo. Maintaining the existing name for backwards compatibility.
|
||||||
|
RenderPartialsWithoutPrefix bool |
||||||
|
} |
||||||
|
|
||||||
|
// HTMLOptions is a struct for overriding some rendering Options for specific HTML call.
|
||||||
|
type HTMLOptions struct { |
||||||
|
// Layout template name. Overrides Options.Layout.
|
||||||
|
Layout string |
||||||
|
// Funcs added to Options.Funcs.
|
||||||
|
Funcs template.FuncMap |
||||||
|
} |
||||||
|
|
||||||
|
// Render is a service that provides functions for easily writing JSON, XML,
|
||||||
|
// binary data, and HTML templates out to a HTTP Response.
|
||||||
|
type Render struct { |
||||||
|
// Customize Secure with an Options struct.
|
||||||
|
opt Options |
||||||
|
templates *template.Template |
||||||
|
templatesLk sync.Mutex |
||||||
|
compiledCharset string |
||||||
|
} |
||||||
|
|
||||||
|
// New constructs a new Render instance with the supplied options.
|
||||||
|
func New(options ...Options) *Render { |
||||||
|
var o Options |
||||||
|
if len(options) > 0 { |
||||||
|
o = options[0] |
||||||
|
} |
||||||
|
|
||||||
|
r := Render{ |
||||||
|
opt: o, |
||||||
|
} |
||||||
|
|
||||||
|
r.prepareOptions() |
||||||
|
r.compileTemplates() |
||||||
|
|
||||||
|
return &r |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Render) prepareOptions() { |
||||||
|
// Fill in the defaults if need be.
|
||||||
|
if len(r.opt.Charset) == 0 { |
||||||
|
r.opt.Charset = defaultCharset |
||||||
|
} |
||||||
|
if r.opt.DisableCharset == false { |
||||||
|
r.compiledCharset = "; charset=" + r.opt.Charset |
||||||
|
} |
||||||
|
|
||||||
|
if len(r.opt.Directory) == 0 { |
||||||
|
r.opt.Directory = "templates" |
||||||
|
} |
||||||
|
if r.opt.FileSystem == nil { |
||||||
|
r.opt.FileSystem = &LocalFileSystem{} |
||||||
|
} |
||||||
|
if len(r.opt.Extensions) == 0 { |
||||||
|
r.opt.Extensions = []string{".tmpl"} |
||||||
|
} |
||||||
|
if len(r.opt.BinaryContentType) == 0 { |
||||||
|
r.opt.BinaryContentType = ContentBinary |
||||||
|
} |
||||||
|
if len(r.opt.HTMLContentType) == 0 { |
||||||
|
r.opt.HTMLContentType = ContentHTML |
||||||
|
} |
||||||
|
if len(r.opt.JSONContentType) == 0 { |
||||||
|
r.opt.JSONContentType = ContentJSON |
||||||
|
} |
||||||
|
if len(r.opt.JSONPContentType) == 0 { |
||||||
|
r.opt.JSONPContentType = ContentJSONP |
||||||
|
} |
||||||
|
if len(r.opt.TextContentType) == 0 { |
||||||
|
r.opt.TextContentType = ContentText |
||||||
|
} |
||||||
|
if len(r.opt.XMLContentType) == 0 { |
||||||
|
r.opt.XMLContentType = ContentXML |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Render) compileTemplates() { |
||||||
|
if r.opt.Asset == nil || r.opt.AssetNames == nil { |
||||||
|
r.compileTemplatesFromDir() |
||||||
|
return |
||||||
|
} |
||||||
|
r.compileTemplatesFromAsset() |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Render) compileTemplatesFromDir() { |
||||||
|
dir := r.opt.Directory |
||||||
|
r.templates = template.New(dir) |
||||||
|
r.templates.Delims(r.opt.Delims.Left, r.opt.Delims.Right) |
||||||
|
|
||||||
|
// Walk the supplied directory and compile any files that match our extension list.
|
||||||
|
r.opt.FileSystem.Walk(dir, func(path string, info os.FileInfo, err error) error { |
||||||
|
// Fix same-extension-dirs bug: some dir might be named to: "users.tmpl", "local.html".
|
||||||
|
// These dirs should be excluded as they are not valid golang templates, but files under
|
||||||
|
// them should be treat as normal.
|
||||||
|
// If is a dir, return immediately (dir is not a valid golang template).
|
||||||
|
if info == nil || info.IsDir() { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
rel, err := filepath.Rel(dir, path) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
ext := "" |
||||||
|
if strings.Index(rel, ".") != -1 { |
||||||
|
ext = filepath.Ext(rel) |
||||||
|
} |
||||||
|
|
||||||
|
for _, extension := range r.opt.Extensions { |
||||||
|
if ext == extension { |
||||||
|
buf, err := r.opt.FileSystem.ReadFile(path) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
name := (rel[0 : len(rel)-len(ext)]) |
||||||
|
tmpl := r.templates.New(filepath.ToSlash(name)) |
||||||
|
|
||||||
|
// Add our funcmaps.
|
||||||
|
for _, funcs := range r.opt.Funcs { |
||||||
|
tmpl.Funcs(funcs) |
||||||
|
} |
||||||
|
|
||||||
|
// Break out if this parsing fails. We don't want any silent server starts.
|
||||||
|
template.Must(tmpl.Funcs(helperFuncs).Parse(string(buf))) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Render) compileTemplatesFromAsset() { |
||||||
|
dir := r.opt.Directory |
||||||
|
r.templates = template.New(dir) |
||||||
|
r.templates.Delims(r.opt.Delims.Left, r.opt.Delims.Right) |
||||||
|
|
||||||
|
for _, path := range r.opt.AssetNames() { |
||||||
|
if !strings.HasPrefix(path, dir) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
rel, err := filepath.Rel(dir, path) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
ext := "" |
||||||
|
if strings.Index(rel, ".") != -1 { |
||||||
|
ext = "." + strings.Join(strings.Split(rel, ".")[1:], ".") |
||||||
|
} |
||||||
|
|
||||||
|
for _, extension := range r.opt.Extensions { |
||||||
|
if ext == extension { |
||||||
|
|
||||||
|
buf, err := r.opt.Asset(path) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
name := (rel[0 : len(rel)-len(ext)]) |
||||||
|
tmpl := r.templates.New(filepath.ToSlash(name)) |
||||||
|
|
||||||
|
// Add our funcmaps.
|
||||||
|
for _, funcs := range r.opt.Funcs { |
||||||
|
tmpl.Funcs(funcs) |
||||||
|
} |
||||||
|
|
||||||
|
// Break out if this parsing fails. We don't want any silent server starts.
|
||||||
|
template.Must(tmpl.Funcs(helperFuncs).Parse(string(buf))) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TemplateLookup is a wrapper around template.Lookup and returns
|
||||||
|
// the template with the given name that is associated with t, or nil
|
||||||
|
// if there is no such template.
|
||||||
|
func (r *Render) TemplateLookup(t string) *template.Template { |
||||||
|
return r.templates.Lookup(t) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Render) execute(name string, binding interface{}) (*bytes.Buffer, error) { |
||||||
|
buf := new(bytes.Buffer) |
||||||
|
return buf, r.templates.ExecuteTemplate(buf, name, binding) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Render) layoutFuncs(name string, binding interface{}) template.FuncMap { |
||||||
|
return template.FuncMap{ |
||||||
|
"yield": func() (template.HTML, error) { |
||||||
|
buf, err := r.execute(name, binding) |
||||||
|
// Return safe HTML here since we are rendering our own template.
|
||||||
|
return template.HTML(buf.String()), err |
||||||
|
}, |
||||||
|
"current": func() (string, error) { |
||||||
|
return name, nil |
||||||
|
}, |
||||||
|
"block": func(partialName string) (template.HTML, error) { |
||||||
|
log.Print("Render's `block` implementation is now depericated. Use `partial` as a drop in replacement.") |
||||||
|
fullPartialName := fmt.Sprintf("%s-%s", partialName, name) |
||||||
|
if r.TemplateLookup(fullPartialName) == nil && r.opt.RenderPartialsWithoutPrefix { |
||||||
|
fullPartialName = partialName |
||||||
|
} |
||||||
|
if r.opt.RequireBlocks || r.TemplateLookup(fullPartialName) != nil { |
||||||
|
buf, err := r.execute(fullPartialName, binding) |
||||||
|
// Return safe HTML here since we are rendering our own template.
|
||||||
|
return template.HTML(buf.String()), err |
||||||
|
} |
||||||
|
return "", nil |
||||||
|
}, |
||||||
|
"partial": func(partialName string) (template.HTML, error) { |
||||||
|
fullPartialName := fmt.Sprintf("%s-%s", partialName, name) |
||||||
|
if r.TemplateLookup(fullPartialName) == nil && r.opt.RenderPartialsWithoutPrefix { |
||||||
|
fullPartialName = partialName |
||||||
|
} |
||||||
|
if r.opt.RequirePartials || r.TemplateLookup(fullPartialName) != nil { |
||||||
|
buf, err := r.execute(fullPartialName, binding) |
||||||
|
// Return safe HTML here since we are rendering our own template.
|
||||||
|
return template.HTML(buf.String()), err |
||||||
|
} |
||||||
|
return "", nil |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *Render) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions { |
||||||
|
layout := r.opt.Layout |
||||||
|
funcs := template.FuncMap{} |
||||||
|
|
||||||
|
for _, tmp := range r.opt.Funcs { |
||||||
|
for k, v := range tmp { |
||||||
|
funcs[k] = v |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(htmlOpt) > 0 { |
||||||
|
opt := htmlOpt[0] |
||||||
|
if len(opt.Layout) > 0 { |
||||||
|
layout = opt.Layout |
||||||
|
} |
||||||
|
|
||||||
|
for k, v := range opt.Funcs { |
||||||
|
funcs[k] = v |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return HTMLOptions{ |
||||||
|
Layout: layout, |
||||||
|
Funcs: funcs, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Render is the generic function called by XML, JSON, Data, HTML, and can be called by custom implementations.
|
||||||
|
func (r *Render) Render(w io.Writer, e Engine, data interface{}) error { |
||||||
|
err := e.Render(w, data) |
||||||
|
if hw, ok := w.(http.ResponseWriter); err != nil && !r.opt.DisableHTTPErrorRendering && ok { |
||||||
|
http.Error(hw, err.Error(), http.StatusInternalServerError) |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Data writes out the raw bytes as binary data.
|
||||||
|
func (r *Render) Data(w io.Writer, status int, v []byte) error { |
||||||
|
head := Head{ |
||||||
|
ContentType: r.opt.BinaryContentType, |
||||||
|
Status: status, |
||||||
|
} |
||||||
|
|
||||||
|
d := Data{ |
||||||
|
Head: head, |
||||||
|
} |
||||||
|
|
||||||
|
return r.Render(w, d, v) |
||||||
|
} |
||||||
|
|
||||||
|
// HTML builds up the response from the specified template and bindings.
|
||||||
|
func (r *Render) HTML(w io.Writer, status int, name string, binding interface{}, htmlOpt ...HTMLOptions) error { |
||||||
|
r.templatesLk.Lock() |
||||||
|
defer r.templatesLk.Unlock() |
||||||
|
|
||||||
|
// If we are in development mode, recompile the templates on every HTML request.
|
||||||
|
if r.opt.IsDevelopment { |
||||||
|
r.compileTemplates() |
||||||
|
} |
||||||
|
|
||||||
|
opt := r.prepareHTMLOptions(htmlOpt) |
||||||
|
if tpl := r.templates.Lookup(name); tpl != nil { |
||||||
|
if len(opt.Layout) > 0 { |
||||||
|
tpl.Funcs(r.layoutFuncs(name, binding)) |
||||||
|
name = opt.Layout |
||||||
|
} |
||||||
|
|
||||||
|
if len(opt.Funcs) > 0 { |
||||||
|
tpl.Funcs(opt.Funcs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
head := Head{ |
||||||
|
ContentType: r.opt.HTMLContentType + r.compiledCharset, |
||||||
|
Status: status, |
||||||
|
} |
||||||
|
|
||||||
|
h := HTML{ |
||||||
|
Head: head, |
||||||
|
Name: name, |
||||||
|
Templates: r.templates, |
||||||
|
} |
||||||
|
|
||||||
|
return r.Render(w, h, binding) |
||||||
|
} |
||||||
|
|
||||||
|
// JSON marshals the given interface object and writes the JSON response.
|
||||||
|
func (r *Render) JSON(w io.Writer, status int, v interface{}) error { |
||||||
|
head := Head{ |
||||||
|
ContentType: r.opt.JSONContentType + r.compiledCharset, |
||||||
|
Status: status, |
||||||
|
} |
||||||
|
|
||||||
|
j := JSON{ |
||||||
|
Head: head, |
||||||
|
Indent: r.opt.IndentJSON, |
||||||
|
Prefix: r.opt.PrefixJSON, |
||||||
|
UnEscapeHTML: r.opt.UnEscapeHTML, |
||||||
|
StreamingJSON: r.opt.StreamingJSON, |
||||||
|
} |
||||||
|
|
||||||
|
return r.Render(w, j, v) |
||||||
|
} |
||||||
|
|
||||||
|
// JSONP marshals the given interface object and writes the JSON response.
|
||||||
|
func (r *Render) JSONP(w io.Writer, status int, callback string, v interface{}) error { |
||||||
|
head := Head{ |
||||||
|
ContentType: r.opt.JSONPContentType + r.compiledCharset, |
||||||
|
Status: status, |
||||||
|
} |
||||||
|
|
||||||
|
j := JSONP{ |
||||||
|
Head: head, |
||||||
|
Indent: r.opt.IndentJSON, |
||||||
|
Callback: callback, |
||||||
|
} |
||||||
|
|
||||||
|
return r.Render(w, j, v) |
||||||
|
} |
||||||
|
|
||||||
|
// Text writes out a string as plain text.
|
||||||
|
func (r *Render) Text(w io.Writer, status int, v string) error { |
||||||
|
head := Head{ |
||||||
|
ContentType: r.opt.TextContentType + r.compiledCharset, |
||||||
|
Status: status, |
||||||
|
} |
||||||
|
|
||||||
|
t := Text{ |
||||||
|
Head: head, |
||||||
|
} |
||||||
|
|
||||||
|
return r.Render(w, t, v) |
||||||
|
} |
||||||
|
|
||||||
|
// XML marshals the given interface object and writes the XML response.
|
||||||
|
func (r *Render) XML(w io.Writer, status int, v interface{}) error { |
||||||
|
head := Head{ |
||||||
|
ContentType: r.opt.XMLContentType + r.compiledCharset, |
||||||
|
Status: status, |
||||||
|
} |
||||||
|
|
||||||
|
x := XML{ |
||||||
|
Head: head, |
||||||
|
Indent: r.opt.IndentXML, |
||||||
|
Prefix: r.opt.PrefixXML, |
||||||
|
} |
||||||
|
|
||||||
|
return r.Render(w, x, v) |
||||||
|
} |
Loading…
Reference in new issue