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>
This commit is contained in:
Lunny Xiao 2021-01-05 21:05:40 +08:00 committed by GitHub
parent 126c9331d6
commit 15a475b7db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 5233 additions and 307 deletions

View file

@ -9,13 +9,10 @@ import (
"reflect"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/sso"
"code.gitea.io/gitea/modules/validation"
"gitea.com/macaron/binding"
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
"github.com/unknwon/com"
)
@ -24,28 +21,6 @@ func IsAPIPath(url string) bool {
return strings.HasPrefix(url, "/api/")
}
// SignedInUser returns the user object of signed user.
// It returns a bool value to indicate whether user uses basic auth or not.
func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) {
if !models.HasEngine {
return nil, false
}
// Try to sign in with each of the enabled plugins
for _, ssoMethod := range sso.Methods() {
if !ssoMethod.IsEnabled() {
continue
}
user := ssoMethod.VerifyAuthData(ctx, sess)
if user != nil {
_, isBasic := ssoMethod.(*sso.Basic)
return user, isBasic
}
}
return nil, false
}
// Form form binding interface
type Form interface {
binding.Validator

View file

@ -6,6 +6,7 @@
package sso
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
@ -13,9 +14,6 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
)
// Ensure the struct implements the interface.
@ -49,8 +47,8 @@ func (b *Basic) IsEnabled() bool {
// "Authorization" header of the request and returns the corresponding user object for that
// name/token on successful validation.
// Returns nil if header is empty or validation fails.
func (b *Basic) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models.User {
baHead := ctx.Req.Header.Get("Authorization")
func (b *Basic) VerifyAuthData(req *http.Request, store DataStore, sess SessionStore) *models.User {
baHead := req.Header.Get("Authorization")
if len(baHead) == 0 {
return nil
}
@ -75,7 +73,7 @@ func (b *Basic) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models
uid := CheckOAuthAccessToken(authToken)
if uid != 0 {
var err error
ctx.Data["IsApiToken"] = true
store.GetData()["IsApiToken"] = true
u, err = models.GetUserByID(uid)
if err != nil {
@ -108,7 +106,7 @@ func (b *Basic) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models
return nil
}
} else {
ctx.Data["IsApiToken"] = true
store.GetData()["IsApiToken"] = true
}
return u

View file

@ -5,12 +5,23 @@
package sso
import (
"code.gitea.io/gitea/models"
"net/http"
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
"code.gitea.io/gitea/models"
)
// DataStore represents a data store
type DataStore interface {
GetData() map[string]interface{}
}
// SessionStore represents a session store
type SessionStore interface {
Get(interface{}) interface{}
Set(interface{}, interface{}) error
Delete(interface{}) error
}
// SingleSignOn represents a SSO authentication method (plugin) for HTTP requests.
type SingleSignOn interface {
// Init should be called exactly once before using any of the other methods,
@ -29,5 +40,5 @@ type SingleSignOn interface {
// or a new user object (with id = 0) populated with the information that was found
// in the authentication data (username or email).
// Returns nil if verification fails.
VerifyAuthData(ctx *macaron.Context, sess session.Store) *models.User
VerifyAuthData(http *http.Request, store DataStore, sess SessionStore) *models.User
}

View file

@ -6,15 +6,13 @@
package sso
import (
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
)
// Ensure the struct implements the interface.
@ -63,15 +61,15 @@ func (o *OAuth2) Free() error {
}
// userIDFromToken returns the user id corresponding to the OAuth token.
func (o *OAuth2) userIDFromToken(ctx *macaron.Context) int64 {
func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 {
// Check access token.
tokenSHA := ctx.Query("token")
tokenSHA := req.Form.Get("token")
if len(tokenSHA) == 0 {
tokenSHA = ctx.Query("access_token")
tokenSHA = req.Form.Get("access_token")
}
if len(tokenSHA) == 0 {
// Well, check with header again.
auHead := ctx.Req.Header.Get("Authorization")
auHead := req.Header.Get("Authorization")
if len(auHead) > 0 {
auths := strings.Fields(auHead)
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
@ -87,7 +85,7 @@ func (o *OAuth2) userIDFromToken(ctx *macaron.Context) int64 {
if strings.Contains(tokenSHA, ".") {
uid := CheckOAuthAccessToken(tokenSHA)
if uid != 0 {
ctx.Data["IsApiToken"] = true
store.GetData()["IsApiToken"] = true
}
return uid
}
@ -102,7 +100,7 @@ func (o *OAuth2) userIDFromToken(ctx *macaron.Context) int64 {
if err = models.UpdateAccessToken(t); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
ctx.Data["IsApiToken"] = true
store.GetData()["IsApiToken"] = true
return t.UID
}
@ -116,16 +114,16 @@ func (o *OAuth2) IsEnabled() bool {
// or the "Authorization" header and returns the corresponding user object for that ID.
// If verification is successful returns an existing user object.
// Returns nil if verification fails.
func (o *OAuth2) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models.User {
func (o *OAuth2) VerifyAuthData(req *http.Request, store DataStore, sess SessionStore) *models.User {
if !models.HasEngine {
return nil
}
if isInternalPath(ctx) || !isAPIPath(ctx) && !isAttachmentDownload(ctx) {
if isInternalPath(req) || !isAPIPath(req) && !isAttachmentDownload(req) {
return nil
}
id := o.userIDFromToken(ctx)
id := o.userIDFromToken(req, store)
if id <= 0 {
return nil
}

View file

@ -6,14 +6,13 @@
package sso
import (
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
gouuid "github.com/google/uuid"
)
@ -31,8 +30,8 @@ type ReverseProxy struct {
}
// getUserName extracts the username from the "setting.ReverseProxyAuthUser" header
func (r *ReverseProxy) getUserName(ctx *macaron.Context) string {
webAuthUser := strings.TrimSpace(ctx.Req.Header.Get(setting.ReverseProxyAuthUser))
func (r *ReverseProxy) getUserName(req *http.Request) string {
webAuthUser := strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthUser))
if len(webAuthUser) == 0 {
return ""
}
@ -61,8 +60,8 @@ func (r *ReverseProxy) IsEnabled() bool {
// If a username is available in the "setting.ReverseProxyAuthUser" header an existing
// user object is returned (populated with username or email found in header).
// Returns nil if header is empty.
func (r *ReverseProxy) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models.User {
username := r.getUserName(ctx)
func (r *ReverseProxy) VerifyAuthData(req *http.Request, store DataStore, sess SessionStore) *models.User {
username := r.getUserName(req)
if len(username) == 0 {
return nil
}
@ -70,7 +69,7 @@ func (r *ReverseProxy) VerifyAuthData(ctx *macaron.Context, sess session.Store)
user, err := models.GetUserByName(username)
if err != nil {
if models.IsErrUserNotExist(err) && r.isAutoRegisterAllowed() {
return r.newUser(ctx)
return r.newUser(req)
}
log.Error("GetUserByName: %v", err)
return nil
@ -86,15 +85,15 @@ func (r *ReverseProxy) isAutoRegisterAllowed() bool {
// newUser creates a new user object for the purpose of automatic registration
// and populates its name and email with the information present in request headers.
func (r *ReverseProxy) newUser(ctx *macaron.Context) *models.User {
username := r.getUserName(ctx)
func (r *ReverseProxy) newUser(req *http.Request) *models.User {
username := r.getUserName(req)
if len(username) == 0 {
return nil
}
email := gouuid.New().String() + "@localhost"
if setting.Service.EnableReverseProxyEmail {
webAuthEmail := ctx.Req.Header.Get(setting.ReverseProxyAuthEmail)
webAuthEmail := req.Header.Get(setting.ReverseProxyAuthEmail)
if len(webAuthEmail) > 0 {
email = webAuthEmail
}

View file

@ -5,10 +5,9 @@
package sso
import (
"code.gitea.io/gitea/models"
"net/http"
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
"code.gitea.io/gitea/models"
)
// Ensure the struct implements the interface.
@ -40,7 +39,7 @@ func (s *Session) IsEnabled() bool {
// VerifyAuthData checks if there is a user uid stored in the session and returns the user
// object for that uid.
// Returns nil if there is no user uid stored in the session.
func (s *Session) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models.User {
func (s *Session) VerifyAuthData(req *http.Request, store DataStore, sess SessionStore) *models.User {
user := SessionUser(sess)
if user != nil {
return user

View file

@ -7,15 +7,14 @@ package sso
import (
"fmt"
"net/http"
"reflect"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/middlewares"
"code.gitea.io/gitea/modules/setting"
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
)
// ssoMethods contains the list of SSO authentication plugins in the order they are expected to be
@ -73,7 +72,7 @@ func Free() {
}
// SessionUser returns the user object corresponding to the "uid" session variable.
func SessionUser(sess session.Store) *models.User {
func SessionUser(sess SessionStore) *models.User {
// Get user ID
uid := sess.Get("uid")
if uid == nil {
@ -96,22 +95,22 @@ func SessionUser(sess session.Store) *models.User {
}
// isAPIPath returns true if the specified URL is an API path
func isAPIPath(ctx *macaron.Context) bool {
return strings.HasPrefix(ctx.Req.URL.Path, "/api/")
func isAPIPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/")
}
// isInternalPath returns true if the specified URL is an internal API path
func isInternalPath(ctx *macaron.Context) bool {
return strings.HasPrefix(ctx.Req.URL.Path, "/api/internal/")
func isInternalPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/internal/")
}
// isAttachmentDownload check if request is a file download (GET) with URL to an attachment
func isAttachmentDownload(ctx *macaron.Context) bool {
return strings.HasPrefix(ctx.Req.URL.Path, "/attachments/") && ctx.Req.Method == "GET"
func isAttachmentDownload(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/attachments/") && req.Method == "GET"
}
// handleSignIn clears existing session variables and stores new ones for the specified user object
func handleSignIn(ctx *macaron.Context, sess session.Store, user *models.User) {
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *models.User) {
_ = sess.Delete("openid_verified_uri")
_ = sess.Delete("openid_signin_remember")
_ = sess.Delete("openid_determined_email")
@ -132,15 +131,16 @@ func handleSignIn(ctx *macaron.Context, sess session.Store, user *models.User) {
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
if len(user.Language) == 0 {
user.Language = ctx.Locale.Language()
lc := middlewares.Locale(resp, req)
user.Language = lc.Language()
if err := models.UpdateUserCols(user, "language"); err != nil {
log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", user.ID, user.Language))
return
}
}
ctx.SetCookie("lang", user.Language, nil, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
middlewares.SetCookie(resp, "lang", user.Language, nil, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
// Clear whatever CSRF has right now, force to generate a new one
ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
middlewares.SetCookie(resp, setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
}

View file

@ -6,6 +6,7 @@ package sso
import (
"errors"
"net/http"
"reflect"
"strings"
@ -64,7 +65,7 @@ func (s *SSPI) IsEnabled() bool {
// If authentication is successful, returs the corresponding user object.
// If negotiation should continue or authentication fails, immediately returns a 401 HTTP
// response code, as required by the SPNEGO protocol.
func (s *SSPI) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models.User {
func (s *SSPI) VerifyAuthData(req *http.Request, store DataStore, sess SessionStore) *models.User {
if !s.shouldAuthenticate(ctx) {
return nil
}
@ -75,7 +76,7 @@ func (s *SSPI) VerifyAuthData(ctx *macaron.Context, sess session.Store) *models.
return nil
}
userInfo, outToken, err := sspiAuth.Authenticate(ctx.Req.Request, ctx.Resp)
userInfo, outToken, err := sspiAuth.Authenticate(req, ctx.Resp)
if err != nil {
log.Warn("Authentication failed with error: %v\n", err)
sspiAuth.AppendAuthenticateHeader(ctx.Resp, outToken)
@ -139,18 +140,18 @@ func (s *SSPI) getConfig() (*models.SSPIConfig, error) {
return sources[0].SSPI(), nil
}
func (s *SSPI) shouldAuthenticate(ctx *macaron.Context) (shouldAuth bool) {
func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
shouldAuth = false
path := strings.TrimSuffix(ctx.Req.URL.Path, "/")
path := strings.TrimSuffix(req.URL.Path, "/")
if path == "/user/login" {
if ctx.Req.FormValue("user_name") != "" && ctx.Req.FormValue("password") != "" {
if req.FormValue("user_name") != "" && req.FormValue("password") != "" {
shouldAuth = false
} else if ctx.Req.FormValue("auth_with_sspi") == "1" {
shouldAuth = true
}
} else if isInternalPath(ctx) {
} else if isInternalPath(req) {
shouldAuth = false
} else if isAPIPath(ctx) || isAttachmentDownload(ctx) {
} else if isAPIPath(req) || isAttachmentDownload(req) {
shouldAuth = true
}
return
@ -158,7 +159,7 @@ func (s *SSPI) shouldAuthenticate(ctx *macaron.Context) (shouldAuth bool) {
// newUser creates a new user object for the purpose of automatic registration
// and populates its name and email with the information present in request headers.
func (s *SSPI) newUser(ctx *macaron.Context, username string, cfg *models.SSPIConfig) (*models.User, error) {
func (s *SSPI) newUser(username string, cfg *models.SSPIConfig) (*models.User, error) {
email := gouuid.New().String() + "@localhost.localdomain"
user := &models.User{
Name: username,

33
modules/auth/sso/user.go Normal file
View file

@ -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
}

View file

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/auth/sso"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@ -48,6 +49,11 @@ type Context struct {
Org *Organization
}
// GetData returns the data
func (ctx *Context) GetData() map[string]interface{} {
return ctx.Data
}
// IsUserSiteAdmin returns true if current user is a site admin
func (ctx *Context) IsUserSiteAdmin() bool {
return ctx.IsSigned && ctx.User.IsAdmin
@ -303,7 +309,7 @@ func Contexter() macaron.Handler {
}
// Get user from session if logged in.
ctx.User, ctx.IsBasicAuth = auth.SignedInUser(ctx.Context, ctx.Session)
ctx.User, ctx.IsBasicAuth = sso.SignedInUser(ctx.Req.Request, ctx, ctx.Session)
if ctx.User != nil {
ctx.IsSigned = true

View file

@ -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()
}
}

View file

@ -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())
}

View file

@ -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)
}

View file

@ -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{})
}

View file

@ -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
}

82
modules/templates/base.go Normal file
View file

@ -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
}

View file

@ -9,7 +9,9 @@ package templates
import (
"html/template"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
texttmpl "text/template"
@ -25,6 +27,25 @@ var (
bodyTemplates = template.New("")
)
// GetAsset returns asset content via name
func GetAsset(name string) ([]byte, error) {
bs, err := ioutil.ReadFile(filepath.Join(setting.CustomPath, name))
if err != nil && !os.IsNotExist(err) {
return nil, err
} else if err == nil {
return bs, nil
}
return ioutil.ReadFile(filepath.Join(setting.StaticRootPath, name))
}
// GetAssetNames returns assets list
func GetAssetNames() []string {
tmpls := getDirAssetNames(filepath.Join(setting.CustomPath, "templates"))
tmpls2 := getDirAssetNames(filepath.Join(setting.StaticRootPath, "templates"))
return append(tmpls, tmpls2...)
}
// HTMLRenderer implements the macaron handler for serving HTML templates.
func HTMLRenderer() macaron.Handler {
return macaron.Renderer(macaron.RenderOptions{

View file

@ -12,7 +12,9 @@ import (
"html/template"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
texttmpl "text/template"
@ -46,6 +48,30 @@ func (templates templateFileSystem) Get(name string) (io.Reader, error) {
return nil, fmt.Errorf("file '%s' not found", name)
}
// GetAsset get a special asset, only for chi
func GetAsset(name string) ([]byte, error) {
bs, err := ioutil.ReadFile(filepath.Join(setting.CustomPath, name))
if err != nil && !os.IsNotExist(err) {
return nil, err
} else if err == nil {
return bs, nil
}
return Asset(strings.TrimPrefix(name, "templates/"))
}
// GetAssetNames only for chi
func GetAssetNames() []string {
realFS := Assets.(vfsgen۰FS)
var tmpls = make([]string, 0, len(realFS))
for k := range realFS {
tmpls = append(tmpls, "templates/"+k[1:])
}
customDir := path.Join(setting.CustomPath, "templates")
customTmpls := getDirAssetNames(customDir)
return append(tmpls, customTmpls...)
}
func NewTemplateFileSystem() templateFileSystem {
fs := templateFileSystem{}
fs.files = make([]macaron.TemplateFile, 0, 10)

View file

@ -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...)
}