Merge branch 'master' into advanced-permissions-phase-1
Этот коммит содержится в:
@@ -11,6 +11,30 @@ import (
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
type TokenLocation int
|
||||
|
||||
const (
|
||||
TokenLocationNotFound = iota
|
||||
TokenLocationHeader
|
||||
TokenLocationCookie
|
||||
TokenLocationQueryString
|
||||
)
|
||||
|
||||
func (tl TokenLocation) String() string {
|
||||
switch tl {
|
||||
case TokenLocationNotFound:
|
||||
return "Not Found"
|
||||
case TokenLocationHeader:
|
||||
return "Header"
|
||||
case TokenLocationCookie:
|
||||
return "Cookie"
|
||||
case TokenLocationQueryString:
|
||||
return "QueryString"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) IsPasswordValid(password string) *model.AppError {
|
||||
if utils.IsLicensed() && *utils.License().Features.PasswordRequirements {
|
||||
return utils.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings)
|
||||
@@ -19,7 +43,7 @@ func (a *App) IsPasswordValid(password string) *model.AppError {
|
||||
}
|
||||
|
||||
func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
|
||||
if err := a.CheckUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil {
|
||||
if err := a.CheckUserPreflightAuthenticationCriteria(user, mfaToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -27,6 +51,10 @@ func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfa
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.CheckUserPostflightAuthenticationCriteria(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -85,13 +113,21 @@ func (a *App) checkLdapUserPasswordAndAllCriteria(ldapId *string, password strin
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *App) CheckUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
|
||||
if err := a.CheckUserMfa(user, mfaToken); err != nil {
|
||||
func (a *App) CheckUserAllAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
|
||||
if err := a.CheckUserPreflightAuthenticationCriteria(user, mfaToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !user.EmailVerified && a.Config().EmailSettings.RequireEmailVerification {
|
||||
return model.NewAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
|
||||
if err := a.CheckUserPostflightAuthenticationCriteria(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) CheckUserPreflightAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
|
||||
if err := a.CheckUserMfa(user, mfaToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkUserNotDisabled(user); err != nil {
|
||||
@@ -105,6 +141,14 @@ func (a *App) CheckUserAdditionalAuthenticationCriteria(user *model.User, mfaTok
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) CheckUserPostflightAuthenticationCriteria(user *model.User) *model.AppError {
|
||||
if !user.EmailVerified && a.Config().EmailSettings.RequireEmailVerification {
|
||||
return model.NewAppError("Login", "api.user.login.not_verified.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) CheckUserMfa(user *model.User, token string) *model.AppError {
|
||||
if !user.MfaActive || !utils.IsLicensed() || !*utils.License().Features.MFA || !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
|
||||
return nil
|
||||
@@ -168,3 +212,26 @@ func (a *App) authenticateUser(user *model.User, password, mfaToken string) (*mo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ParseAuthTokenFromRequest(r *http.Request) (string, TokenLocation) {
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
|
||||
// Default session token
|
||||
return authHeader[7:], TokenLocationHeader
|
||||
} else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
|
||||
// OAuth token
|
||||
return authHeader[6:], TokenLocationHeader
|
||||
}
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
return cookie.Value, TokenLocationCookie
|
||||
}
|
||||
|
||||
// Attempt to parse token out of the query string
|
||||
if token := r.URL.Query().Get("access_token"); token != "" {
|
||||
return token, TokenLocationQueryString
|
||||
}
|
||||
|
||||
return "", TokenLocationNotFound
|
||||
}
|
||||
|
||||
52
app/authentication_test.go
Обычный файл
52
app/authentication_test.go
Обычный файл
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseAuthTokenFromRequest(t *testing.T) {
|
||||
cases := []struct {
|
||||
header string
|
||||
cookie string
|
||||
query string
|
||||
expectedToken string
|
||||
expectedLocation TokenLocation
|
||||
}{
|
||||
{"", "", "", "", TokenLocationNotFound},
|
||||
{"token mytoken", "", "", "mytoken", TokenLocationHeader},
|
||||
{"BEARER mytoken", "", "", "mytoken", TokenLocationHeader},
|
||||
{"", "mytoken", "", "mytoken", TokenLocationCookie},
|
||||
{"", "", "mytoken", "mytoken", TokenLocationQueryString},
|
||||
}
|
||||
|
||||
for testnum, tc := range cases {
|
||||
pathname := "/test/here"
|
||||
if tc.query != "" {
|
||||
pathname += "?access_token=" + tc.query
|
||||
}
|
||||
req := httptest.NewRequest("GET", pathname, nil)
|
||||
if tc.header != "" {
|
||||
req.Header.Add(model.HEADER_AUTH, tc.header)
|
||||
}
|
||||
if tc.cookie != "" {
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: model.SESSION_COOKIE_TOKEN,
|
||||
Value: tc.cookie,
|
||||
})
|
||||
}
|
||||
|
||||
token, location := ParseAuthTokenFromRequest(req)
|
||||
|
||||
require.Equal(t, tc.expectedToken, token, "Wrong token on test "+strconv.Itoa(testnum))
|
||||
require.Equal(t, tc.expectedLocation, location, "Wrong location on test "+strconv.Itoa(testnum))
|
||||
}
|
||||
}
|
||||
@@ -1259,6 +1259,14 @@ func (a *App) UpdateChannelLastViewedAt(channelIds []string, userId string) *mod
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) AutocompleteChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
|
||||
if result := <-a.Srv.Store.Channel().AutocompleteInTeam(teamId, term); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.ChannelList), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
|
||||
if result := <-a.Srv.Store.Channel().SearchInTeam(teamId, term); result.Err != nil {
|
||||
return nil, result.Err
|
||||
|
||||
@@ -241,6 +241,9 @@ func (a *App) trackConfig() {
|
||||
"enable_tutorial": *cfg.ServiceSettings.EnableTutorial,
|
||||
"experimental_enable_default_channel_leave_join_messages": *cfg.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages,
|
||||
"experimental_group_unread_channels": *cfg.ServiceSettings.ExperimentalGroupUnreadChannels,
|
||||
"isdefault_image_proxy_type": isDefault(*cfg.ServiceSettings.ImageProxyType, ""),
|
||||
"isdefault_image_proxy_url": isDefault(*cfg.ServiceSettings.ImageProxyURL, ""),
|
||||
"isdefault_image_proxy_options": isDefault(*cfg.ServiceSettings.ImageProxyOptions, ""),
|
||||
})
|
||||
|
||||
a.SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{
|
||||
@@ -347,7 +350,8 @@ func (a *App) trackConfig() {
|
||||
|
||||
a.SendDiagnostic(TRACK_CONFIG_RATE, map[string]interface{}{
|
||||
"enable_rate_limiter": *cfg.RateLimitSettings.Enable,
|
||||
"vary_by_remote_address": cfg.RateLimitSettings.VaryByRemoteAddr,
|
||||
"vary_by_remote_address": *cfg.RateLimitSettings.VaryByRemoteAddr,
|
||||
"vary_by_user": *cfg.RateLimitSettings.VaryByUser,
|
||||
"per_sec": *cfg.RateLimitSettings.PerSec,
|
||||
"max_burst": *cfg.RateLimitSettings.MaxBurst,
|
||||
"memory_store_size": *cfg.RateLimitSettings.MemoryStoreSize,
|
||||
|
||||
@@ -584,16 +584,16 @@ func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *
|
||||
msg.ChannelName = channel.Name
|
||||
msg.SenderId = post.UserId
|
||||
|
||||
if ou, ok := post.Props["override_username"]; ok && ou != nil {
|
||||
msg.OverrideUsername = ou.(string)
|
||||
if ou, ok := post.Props["override_username"].(string); ok {
|
||||
msg.OverrideUsername = ou
|
||||
}
|
||||
|
||||
if oi, ok := post.Props["override_icon_url"]; ok && oi != nil {
|
||||
msg.OverrideIconUrl = oi.(string)
|
||||
if oi, ok := post.Props["override_icon_url"].(string); ok {
|
||||
msg.OverrideIconUrl = oi
|
||||
}
|
||||
|
||||
if fw, ok := post.Props["from_webhook"]; ok && fw != nil {
|
||||
msg.FromWebhook = fw.(string)
|
||||
if fw, ok := post.Props["from_webhook"].(string); ok {
|
||||
msg.FromWebhook = fw
|
||||
}
|
||||
|
||||
if *a.Config().EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION {
|
||||
|
||||
@@ -564,7 +564,7 @@ func generateOAuthStateTokenExtra(email, action, cookie string) string {
|
||||
|
||||
func (a *App) GetAuthorizationCode(w http.ResponseWriter, r *http.Request, service string, props map[string]string, loginHint string) (string, *model.AppError) {
|
||||
sso := a.Config().GetSSOService(service)
|
||||
if sso != nil && !sso.Enable {
|
||||
if sso == nil || !sso.Enable {
|
||||
return "", model.NewAppError("GetAuthorizationCode", "api.user.get_authorization_code.unsupported.app_error", nil, "service="+service, http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -890,7 +890,7 @@ func (a *App) ImageProxyAdder() func(string) string {
|
||||
}
|
||||
|
||||
return func(url string) string {
|
||||
if strings.HasPrefix(url, proxyURL) {
|
||||
if url == "" || strings.HasPrefix(url, proxyURL) {
|
||||
return url
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,12 @@ func TestImageProxy(t *testing.T) {
|
||||
ImageURL: "http://mydomain.com/myimage",
|
||||
ProxiedImageURL: "https://127.0.0.1/x1000/http://mydomain.com/myimage",
|
||||
},
|
||||
"willnorris/imageproxy_EmptyImageURL": {
|
||||
ProxyType: "willnorris/imageproxy",
|
||||
ProxyURL: "https://127.0.0.1",
|
||||
ImageURL: "",
|
||||
ProxiedImageURL: "",
|
||||
},
|
||||
"willnorris/imageproxy_WithSigning": {
|
||||
ProxyType: "willnorris/imageproxy",
|
||||
ProxyURL: "https://127.0.0.1",
|
||||
|
||||
130
app/ratelimit.go
Обычный файл
130
app/ratelimit.go
Обычный файл
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"github.com/pkg/errors"
|
||||
throttled "gopkg.in/throttled/throttled.v2"
|
||||
"gopkg.in/throttled/throttled.v2/store/memstore"
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
throttledRateLimiter *throttled.GCRARateLimiter
|
||||
useAuth bool
|
||||
useIP bool
|
||||
header string
|
||||
}
|
||||
|
||||
func NewRateLimiter(settings *model.RateLimitSettings) (*RateLimiter, error) {
|
||||
store, err := memstore.New(*settings.MemoryStoreSize)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, utils.T("api.server.start_server.rate_limiting_memory_store"))
|
||||
}
|
||||
|
||||
quota := throttled.RateQuota{
|
||||
MaxRate: throttled.PerSec(*settings.PerSec),
|
||||
MaxBurst: *settings.MaxBurst,
|
||||
}
|
||||
|
||||
throttledRateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, utils.T("api.server.start_server.rate_limiting_rate_limiter"))
|
||||
}
|
||||
|
||||
return &RateLimiter{
|
||||
throttledRateLimiter: throttledRateLimiter,
|
||||
useAuth: *settings.VaryByUser,
|
||||
useIP: *settings.VaryByRemoteAddr,
|
||||
header: settings.VaryByHeader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) GenerateKey(r *http.Request) string {
|
||||
key := ""
|
||||
|
||||
if rl.useAuth {
|
||||
token, tokenLocation := ParseAuthTokenFromRequest(r)
|
||||
if tokenLocation != TokenLocationNotFound {
|
||||
key += token
|
||||
} else if rl.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP
|
||||
key += utils.GetIpAddress(r)
|
||||
}
|
||||
} else if rl.useIP { // Only if Auth based is not enabed do we use a plain IP based
|
||||
key += utils.GetIpAddress(r)
|
||||
}
|
||||
|
||||
// Note that most of the time the user won't have to set this because the utils.GetIpAddress above tries the
|
||||
// most common headers anyway.
|
||||
if rl.header != "" {
|
||||
key += strings.ToLower(r.Header.Get(rl.header))
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) RateLimitWriter(key string, w http.ResponseWriter) bool {
|
||||
limited, context, err := rl.throttledRateLimiter.RateLimit(key, 1)
|
||||
if err != nil {
|
||||
l4g.Critical("Internal server error when rate limiting. Rate Limiting broken. Error:" + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
setRateLimitHeaders(w, context)
|
||||
|
||||
if limited {
|
||||
l4g.Error("Denied due to throttling settings code=429 key=%v", key)
|
||||
http.Error(w, "limit exceeded", 429)
|
||||
}
|
||||
|
||||
return limited
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) UserIdRateLimit(userId string, w http.ResponseWriter) bool {
|
||||
if rl.useAuth {
|
||||
if rl.RateLimitWriter(userId, w) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) RateLimitHandler(wrappedHandler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := rl.GenerateKey(r)
|
||||
limited := rl.RateLimitWriter(key, w)
|
||||
|
||||
if !limited {
|
||||
wrappedHandler.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Copied from https://github.com/throttled/throttled http.go
|
||||
func setRateLimitHeaders(w http.ResponseWriter, context throttled.RateLimitResult) {
|
||||
if v := context.Limit; v >= 0 {
|
||||
w.Header().Add("X-RateLimit-Limit", strconv.Itoa(v))
|
||||
}
|
||||
|
||||
if v := context.Remaining; v >= 0 {
|
||||
w.Header().Add("X-RateLimit-Remaining", strconv.Itoa(v))
|
||||
}
|
||||
|
||||
if v := context.ResetAfter; v >= 0 {
|
||||
vi := int(math.Ceil(v.Seconds()))
|
||||
w.Header().Add("X-RateLimit-Reset", strconv.Itoa(vi))
|
||||
}
|
||||
|
||||
if v := context.RetryAfter; v >= 0 {
|
||||
vi := int(math.Ceil(v.Seconds()))
|
||||
w.Header().Add("Retry-After", strconv.Itoa(vi))
|
||||
}
|
||||
}
|
||||
82
app/ratelimit_test.go
Обычный файл
82
app/ratelimit_test.go
Обычный файл
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func genRateLimitSettings(useAuth, useIP bool, header string) *model.RateLimitSettings {
|
||||
return &model.RateLimitSettings{
|
||||
Enable: model.NewBool(true),
|
||||
PerSec: model.NewInt(10),
|
||||
MaxBurst: model.NewInt(100),
|
||||
MemoryStoreSize: model.NewInt(10000),
|
||||
VaryByRemoteAddr: model.NewBool(useIP),
|
||||
VaryByUser: model.NewBool(useAuth),
|
||||
VaryByHeader: header,
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRateLimiterSuccess(t *testing.T) {
|
||||
settings := genRateLimitSettings(false, false, "")
|
||||
rateLimiter, err := NewRateLimiter(settings)
|
||||
require.NotNil(t, rateLimiter)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNewRateLimiterFailure(t *testing.T) {
|
||||
invalidSettings := genRateLimitSettings(false, false, "")
|
||||
invalidSettings.MaxBurst = model.NewInt(-100)
|
||||
rateLimiter, err := NewRateLimiter(invalidSettings)
|
||||
require.Nil(t, rateLimiter)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGenerateKey(t *testing.T) {
|
||||
cases := []struct {
|
||||
useAuth bool
|
||||
useIP bool
|
||||
header string
|
||||
authTokenResult string
|
||||
ipResult string
|
||||
headerResult string
|
||||
expectedKey string
|
||||
}{
|
||||
{false, false, "", "", "", "", ""},
|
||||
{true, false, "", "resultkey", "notme", "notme", "resultkey"},
|
||||
{false, true, "", "notme", "resultkey", "notme", "resultkey"},
|
||||
{false, false, "myheader", "notme", "notme", "resultkey", "resultkey"},
|
||||
{true, true, "", "resultkey", "ipaddr", "notme", "resultkey"},
|
||||
{true, true, "", "", "ipaddr", "notme", "ipaddr"},
|
||||
{true, true, "myheader", "resultkey", "ipaddr", "hadd", "resultkeyhadd"},
|
||||
{true, true, "myheader", "", "ipaddr", "hadd", "ipaddrhadd"},
|
||||
}
|
||||
|
||||
for testnum, tc := range cases {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
if tc.authTokenResult != "" {
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: model.SESSION_COOKIE_TOKEN,
|
||||
Value: tc.authTokenResult,
|
||||
})
|
||||
}
|
||||
req.RemoteAddr = tc.ipResult + ":80"
|
||||
if tc.headerResult != "" {
|
||||
req.Header.Set(tc.header, tc.headerResult)
|
||||
}
|
||||
|
||||
rateLimiter, _ := NewRateLimiter(genRateLimitSettings(tc.useAuth, tc.useIP, tc.header))
|
||||
|
||||
key := rateLimiter.GenerateKey(req)
|
||||
|
||||
require.Equal(t, tc.expectedKey, key, "Wrong key on test "+strconv.Itoa(testnum))
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,14 @@ import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rsc/letsencrypt"
|
||||
"gopkg.in/throttled/throttled.v2"
|
||||
"gopkg.in/throttled/throttled.v2/store/memstore"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
@@ -31,6 +30,7 @@ type Server struct {
|
||||
Router *mux.Router
|
||||
Server *http.Server
|
||||
ListenAddr *net.TCPAddr
|
||||
RateLimiter *RateLimiter
|
||||
|
||||
didFinishListen chan struct{}
|
||||
}
|
||||
@@ -83,10 +83,26 @@ func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second
|
||||
|
||||
type VaryBy struct{}
|
||||
type VaryBy struct {
|
||||
useIP bool
|
||||
useAuth bool
|
||||
}
|
||||
|
||||
func (m *VaryBy) Key(r *http.Request) string {
|
||||
return utils.GetIpAddress(r)
|
||||
key := ""
|
||||
|
||||
if m.useAuth {
|
||||
token, tokenLocation := ParseAuthTokenFromRequest(r)
|
||||
if tokenLocation != TokenLocationNotFound {
|
||||
key += token
|
||||
} else if m.useIP { // If we don't find an authentication token and IP based is enabled, fall back to IP
|
||||
key += utils.GetIpAddress(r)
|
||||
}
|
||||
} else if m.useIP { // Only if Auth based is not enabed do we use a plain IP based
|
||||
key = utils.GetIpAddress(r)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -108,33 +124,14 @@ func (a *App) StartServer() {
|
||||
if *a.Config().RateLimitSettings.Enable {
|
||||
l4g.Info(utils.T("api.server.start_server.rate.info"))
|
||||
|
||||
store, err := memstore.New(*a.Config().RateLimitSettings.MemoryStoreSize)
|
||||
rateLimiter, err := NewRateLimiter(&a.Config().RateLimitSettings)
|
||||
if err != nil {
|
||||
l4g.Critical(utils.T("api.server.start_server.rate_limiting_memory_store"))
|
||||
l4g.Critical(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
quota := throttled.RateQuota{
|
||||
MaxRate: throttled.PerSec(*a.Config().RateLimitSettings.PerSec),
|
||||
MaxBurst: *a.Config().RateLimitSettings.MaxBurst,
|
||||
}
|
||||
|
||||
rateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
|
||||
if err != nil {
|
||||
l4g.Critical(utils.T("api.server.start_server.rate_limiting_rate_limiter"))
|
||||
return
|
||||
}
|
||||
|
||||
httpRateLimiter := throttled.HTTPRateLimiter{
|
||||
RateLimiter: rateLimiter,
|
||||
VaryBy: &VaryBy{},
|
||||
DeniedHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l4g.Error("%v: Denied due to throttling settings code=429 ip=%v", r.URL.Path, utils.GetIpAddress(r))
|
||||
throttled.DefaultDeniedHandler.ServeHTTP(w, r)
|
||||
}),
|
||||
}
|
||||
|
||||
handler = httpRateLimiter.RateLimit(handler)
|
||||
a.Srv.RateLimiter = rateLimiter
|
||||
handler = rateLimiter.RateLimitHandler(handler)
|
||||
}
|
||||
|
||||
a.Srv.Server = &http.Server{
|
||||
@@ -161,18 +158,34 @@ func (a *App) StartServer() {
|
||||
|
||||
l4g.Info(utils.T("api.server.start_server.listening.info"), listener.Addr().String())
|
||||
|
||||
if *a.Config().ServiceSettings.Forward80To443 {
|
||||
go func() {
|
||||
redirectListener, err := net.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
l4g.Error("Unable to setup forwarding: " + err.Error())
|
||||
return
|
||||
}
|
||||
defer redirectListener.Close()
|
||||
// Migration from old let's encrypt library
|
||||
if *a.Config().ServiceSettings.UseLetsEncrypt {
|
||||
if stat, err := os.Stat(*a.Config().ServiceSettings.LetsEncryptCertificateCacheFile); err == nil && !stat.IsDir() {
|
||||
os.Remove(*a.Config().ServiceSettings.LetsEncryptCertificateCacheFile)
|
||||
}
|
||||
}
|
||||
|
||||
http.Serve(redirectListener, http.HandlerFunc(redirectHTTPToHTTPS))
|
||||
}()
|
||||
m := &autocert.Manager{
|
||||
Cache: autocert.DirCache(*a.Config().ServiceSettings.LetsEncryptCertificateCacheFile),
|
||||
Prompt: autocert.AcceptTOS,
|
||||
}
|
||||
|
||||
if *a.Config().ServiceSettings.Forward80To443 {
|
||||
if *a.Config().ServiceSettings.UseLetsEncrypt {
|
||||
go http.ListenAndServe(":http", m.HTTPHandler(nil))
|
||||
} else {
|
||||
go func() {
|
||||
redirectListener, err := net.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
l4g.Error("Unable to setup forwarding: " + err.Error())
|
||||
return
|
||||
}
|
||||
defer redirectListener.Close()
|
||||
|
||||
http.Serve(redirectListener, http.HandlerFunc(redirectHTTPToHTTPS))
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
a.Srv.didFinishListen = make(chan struct{})
|
||||
@@ -180,8 +193,6 @@ func (a *App) StartServer() {
|
||||
var err error
|
||||
if *a.Config().ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS {
|
||||
if *a.Config().ServiceSettings.UseLetsEncrypt {
|
||||
var m letsencrypt.Manager
|
||||
m.CacheFile(*a.Config().ServiceSettings.LetsEncryptCertificateCacheFile)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
GetCertificate: m.GetCertificate,
|
||||
|
||||
14
app/team.go
14
app/team.go
@@ -104,7 +104,7 @@ func (a *App) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
a.sendUpdatedTeamEvent(oldTeam)
|
||||
a.sendTeamEvent(oldTeam, model.WEBSOCKET_EVENT_UPDATE_TEAM)
|
||||
|
||||
return oldTeam, nil
|
||||
}
|
||||
@@ -122,17 +122,17 @@ func (a *App) PatchTeam(teamId string, patch *model.TeamPatch) (*model.Team, *mo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.sendUpdatedTeamEvent(updatedTeam)
|
||||
a.sendTeamEvent(updatedTeam, model.WEBSOCKET_EVENT_UPDATE_TEAM)
|
||||
|
||||
return updatedTeam, nil
|
||||
}
|
||||
|
||||
func (a *App) sendUpdatedTeamEvent(team *model.Team) {
|
||||
func (a *App) sendTeamEvent(team *model.Team, event string) {
|
||||
sanitizedTeam := &model.Team{}
|
||||
*sanitizedTeam = *team
|
||||
sanitizedTeam.Sanitize()
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_UPDATE_TEAM, "", "", "", nil)
|
||||
message := model.NewWebSocketEvent(event, "", "", "", nil)
|
||||
message.Add("team", sanitizedTeam.ToJson())
|
||||
a.Go(func() {
|
||||
a.Publish(message)
|
||||
@@ -685,7 +685,7 @@ func (a *App) postRemoveFromTeamMessage(user *model.User, channel *model.Channel
|
||||
Type: model.POST_REMOVE_FROM_TEAM,
|
||||
UserId: user.Id,
|
||||
Props: model.StringInterface{
|
||||
"removedUsername": user.Username,
|
||||
"username": user.Username,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -824,6 +824,8 @@ func (a *App) PermanentDeleteTeam(team *model.Team) *model.AppError {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
a.sendTeamEvent(team, model.WEBSOCKET_EVENT_DELETE_TEAM)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -838,6 +840,8 @@ func (a *App) SoftDeleteTeam(teamId string) *model.AppError {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
a.sendTeamEvent(team, model.WEBSOCKET_EVENT_DELETE_TEAM)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user