diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2021-01-05 21:05:40 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-05 21:05:40 +0800 |
commit | 15a475b7dbcf7923d9518dff7764b20e404eb774 (patch) | |
tree | 8789f1f82c5e41345b442df4e58120bdd5f8bade /modules/middlewares | |
parent | 126c9331d6d8789563fae5d5bac2196d63fee0e8 (diff) | |
download | gitea-15a475b7dbcf7923d9518dff7764b20e404eb774.tar.gz gitea-15a475b7dbcf7923d9518dff7764b20e404eb774.zip |
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>
Diffstat (limited to 'modules/middlewares')
-rw-r--r-- | modules/middlewares/cookie.go | 104 | ||||
-rw-r--r-- | modules/middlewares/locale.go | 49 | ||||
-rw-r--r-- | modules/middlewares/redis.go | 217 | ||||
-rw-r--r-- | modules/middlewares/virtual.go | 196 |
4 files changed, 566 insertions, 0 deletions
diff --git a/modules/middlewares/cookie.go b/modules/middlewares/cookie.go new file mode 100644 index 0000000000..80d0e3b453 --- /dev/null +++ b/modules/middlewares/cookie.go @@ -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()) +} diff --git a/modules/middlewares/locale.go b/modules/middlewares/locale.go new file mode 100644 index 0000000000..98af890cfd --- /dev/null +++ b/modules/middlewares/locale.go @@ -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) +} diff --git a/modules/middlewares/redis.go b/modules/middlewares/redis.go new file mode 100644 index 0000000000..ced1c1ee81 --- /dev/null +++ b/modules/middlewares/redis.go @@ -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{}) +} diff --git a/modules/middlewares/virtual.go b/modules/middlewares/virtual.go new file mode 100644 index 0000000000..70d780d65d --- /dev/null +++ b/modules/middlewares/virtual.go @@ -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 +} |