mirror of https://github.com/go-gitea/gitea
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>pull/14243/head^2
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