原始提交
This commit is contained in:
		
							
								
								
									
										89
									
								
								service/session_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								service/session_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU Affero General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"yggdrasil-go/model"
 | 
			
		||||
	"yggdrasil-go/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SessionService interface {
 | 
			
		||||
	JoinServer(accessToken string, serverId string, selectedProfile string, ip string) error
 | 
			
		||||
	HasJoinedServer(serverId string, username string, ip string, textureBaseUrl string) (map[string]interface{}, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type sessionStore struct {
 | 
			
		||||
	sessionCache *lru.Cache
 | 
			
		||||
	tokenService TokenService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSessionService(service TokenService) SessionService {
 | 
			
		||||
	cache, _ := lru.New(100000)
 | 
			
		||||
	store := sessionStore{
 | 
			
		||||
		sessionCache: cache,
 | 
			
		||||
		tokenService: service,
 | 
			
		||||
	}
 | 
			
		||||
	return &store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *sessionStore) JoinServer(accessToken string, serverId string, selectedProfile string, ip string) error {
 | 
			
		||||
	token, ok := s.tokenService.GetToken(accessToken)
 | 
			
		||||
	if ok && util.UnsignedString(token.SelectedProfile.Id) == selectedProfile {
 | 
			
		||||
		session := model.NewAuthenticationSession(serverId, token, ip)
 | 
			
		||||
		s.sessionCache.Add(serverId, &session)
 | 
			
		||||
	} else {
 | 
			
		||||
		data := map[string]string{
 | 
			
		||||
			"accessToken":     accessToken,
 | 
			
		||||
			"selectedProfile": selectedProfile,
 | 
			
		||||
			"serverId":        serverId,
 | 
			
		||||
		}
 | 
			
		||||
		err := util.PostObjectForError("https://sessionserver.mojang.com/session/minecraft/join", data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *sessionStore) HasJoinedServer(serverId string, username string, ip string, textureBaseUrl string) (map[string]interface{}, error) {
 | 
			
		||||
	if value, ok := s.sessionCache.Get(serverId); ok {
 | 
			
		||||
		if session, ok := value.(*model.AuthenticationSession); ok {
 | 
			
		||||
			if !(session.HasExpired() && s.sessionCache.Remove(serverId)) &&
 | 
			
		||||
				(ip == session.Ip) && (session.Token.SelectedProfile.Name == username) {
 | 
			
		||||
				return session.Token.SelectedProfile.ToCompleteResponse(true, textureBaseUrl)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		m := make(map[string]interface{})
 | 
			
		||||
		includeIp := ""
 | 
			
		||||
		if ip != "" {
 | 
			
		||||
			includeIp = "&ip=" + url.QueryEscape(ip)
 | 
			
		||||
		}
 | 
			
		||||
		err := util.GetObject(fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s%s", url.QueryEscape(username), url.QueryEscape(serverId), includeIp), &m)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			return m, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil, util.YggdrasilError{Status: http.StatusNoContent}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										246
									
								
								service/texture_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								service/texture_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,246 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU Affero General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"image"
 | 
			
		||||
	_ "image/jpeg"
 | 
			
		||||
	"image/png"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"yggdrasil-go/model"
 | 
			
		||||
	"yggdrasil-go/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TextureService interface {
 | 
			
		||||
	GetTexture(hash string) ([]byte, error)
 | 
			
		||||
	SetTexture(accessToken string, profileId uuid.UUID, skinUrl string, textureType string, model *model.ModelType) error
 | 
			
		||||
	UploadTexture(accessToken string, profileId uuid.UUID, skinReader io.Reader, textureType string, model *model.ModelType) error
 | 
			
		||||
	DeleteTexture(accessToken string, profileId uuid.UUID, textureType string) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type textureServiceImpl struct {
 | 
			
		||||
	tokenService TokenService
 | 
			
		||||
	db           *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTextureService(tokenService TokenService, db *gorm.DB) TextureService {
 | 
			
		||||
	textureService := textureServiceImpl{
 | 
			
		||||
		tokenService: tokenService,
 | 
			
		||||
		db:           db,
 | 
			
		||||
	}
 | 
			
		||||
	return &textureService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *textureServiceImpl) GetTexture(hash string) ([]byte, error) {
 | 
			
		||||
	texture := model.Texture{}
 | 
			
		||||
	if err := t.db.First(&texture, "hash = ?", hash).Error; err == nil {
 | 
			
		||||
		return texture.Data, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		err := util.YggdrasilError{
 | 
			
		||||
			Status:       http.StatusNotFound,
 | 
			
		||||
			ErrorCode:    "Not Found",
 | 
			
		||||
			ErrorMessage: "Texture Not Found",
 | 
			
		||||
		}
 | 
			
		||||
		return nil, &err
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *textureServiceImpl) SetTexture(accessToken string, profileId uuid.UUID, skinUrl string, textureType string, modelType *model.ModelType) error {
 | 
			
		||||
	token, ok := t.tokenService.GetToken(accessToken)
 | 
			
		||||
	if !ok || token.GetAvailableLevel() != model.Valid {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
	}
 | 
			
		||||
	if token.SelectedProfile.Id != profileId {
 | 
			
		||||
		return util.NewForbiddenOperationError("Profile mismatch.")
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	if err := t.db.First(&user, profileId).Error; err != nil {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
			
		||||
	}
 | 
			
		||||
	skinDownloadUrl, err := url.Parse(skinUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return util.NewIllegalArgumentError("Invalid skin url: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	response, err := http.Get(skinDownloadUrl.String())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer response.Body.Close()
 | 
			
		||||
	if response.ContentLength > 1048576 {
 | 
			
		||||
		return util.NewIllegalArgumentError("File too large(more than 1MiB)")
 | 
			
		||||
	}
 | 
			
		||||
	reader := io.LimitReader(response.Body, 1048576)
 | 
			
		||||
	var header bytes.Buffer
 | 
			
		||||
	conf, _, err := image.DecodeConfig(io.TeeReader(reader, &header))
 | 
			
		||||
	if err != nil || conf.Width > 1024 || conf.Height > 1024 {
 | 
			
		||||
		return util.NewIllegalArgumentError("Image too large(max 1024 pixels each dimension)")
 | 
			
		||||
	}
 | 
			
		||||
	im, _, err := image.Decode(io.MultiReader(&header, reader))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = t.saveTexture(&user, im, textureType, modelType)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		profile, _ := user.Profile()
 | 
			
		||||
		token.SelectedProfile = *profile
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *textureServiceImpl) UploadTexture(accessToken string, profileId uuid.UUID, skinReader io.Reader, textureType string, modelType *model.ModelType) error {
 | 
			
		||||
	token, ok := t.tokenService.GetToken(accessToken)
 | 
			
		||||
	if !ok || token.GetAvailableLevel() != model.Valid {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
	}
 | 
			
		||||
	if token.SelectedProfile.Id != profileId {
 | 
			
		||||
		return util.NewForbiddenOperationError("Profile mismatch.")
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	if err := t.db.First(&user, profileId).Error; err != nil {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
			
		||||
	}
 | 
			
		||||
	reader := io.LimitReader(skinReader, 1048576)
 | 
			
		||||
	var header bytes.Buffer
 | 
			
		||||
	conf, _, err := image.DecodeConfig(io.TeeReader(reader, &header))
 | 
			
		||||
	if err != nil || conf.Width > 1024 || conf.Height > 1024 {
 | 
			
		||||
		return util.NewIllegalArgumentError("Image too large(max 1024 pixels each dimension)")
 | 
			
		||||
	}
 | 
			
		||||
	im, _, err := image.Decode(io.MultiReader(&header, reader))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = t.saveTexture(&user, im, textureType, modelType)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	} else {
 | 
			
		||||
		profile, _ := user.Profile()
 | 
			
		||||
		token.SelectedProfile = *profile
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *textureServiceImpl) DeleteTexture(accessToken string, profileId uuid.UUID, textureType string) error {
 | 
			
		||||
	token, ok := t.tokenService.GetToken(accessToken)
 | 
			
		||||
	if !ok || token.GetAvailableLevel() != model.Valid {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
	}
 | 
			
		||||
	if token.SelectedProfile.Id != profileId {
 | 
			
		||||
		return util.NewForbiddenOperationError("Profile mismatch.")
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	if err := t.db.First(&user, profileId).Error; err != nil {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
			
		||||
	}
 | 
			
		||||
	textureType = strings.ToUpper(textureType)
 | 
			
		||||
	if textureType != "SKIN" && textureType != "CAPE" {
 | 
			
		||||
		textureType = "SKIN"
 | 
			
		||||
	}
 | 
			
		||||
	var profile *model.Profile
 | 
			
		||||
	hash, ok := token.SelectedProfile.Textures[textureType]
 | 
			
		||||
	if ok {
 | 
			
		||||
		delete(token.SelectedProfile.Textures, textureType)
 | 
			
		||||
		profile = &token.SelectedProfile
 | 
			
		||||
	} else {
 | 
			
		||||
		p, err := user.Profile()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		hash, ok = p.Textures[textureType]
 | 
			
		||||
		if ok {
 | 
			
		||||
			delete(p.Textures, textureType)
 | 
			
		||||
		} else {
 | 
			
		||||
			return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
			
		||||
		}
 | 
			
		||||
		profile = p
 | 
			
		||||
	}
 | 
			
		||||
	return t.db.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		texture := model.Texture{}
 | 
			
		||||
		if err := tx.Select("hash", "used").First(&texture, "hash = ?", hash).Error; err == nil {
 | 
			
		||||
			if texture.Used < 2 {
 | 
			
		||||
				tx.Delete(&texture)
 | 
			
		||||
			} else {
 | 
			
		||||
				tx.Model(&texture).Update("used", gorm.Expr("used - ?", 1))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		user.SetProfile(profile)
 | 
			
		||||
		return tx.Save(&user).Error
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *textureServiceImpl) saveTexture(user *model.User, skinImage image.Image, textureType string, modelType *model.ModelType) error {
 | 
			
		||||
	var modelValue model.ModelType
 | 
			
		||||
	if modelType != nil && *modelType == model.ALEX {
 | 
			
		||||
		modelValue = *modelType
 | 
			
		||||
	} else {
 | 
			
		||||
		modelValue = model.STEVE
 | 
			
		||||
	}
 | 
			
		||||
	textureType = strings.ToUpper(textureType)
 | 
			
		||||
	if textureType != "SKIN" && textureType != "CAPE" {
 | 
			
		||||
		textureType = "SKIN"
 | 
			
		||||
	}
 | 
			
		||||
	return t.db.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		profile, err := user.Profile()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if textureType == "SKIN" {
 | 
			
		||||
			profile.ModelType = modelValue
 | 
			
		||||
		}
 | 
			
		||||
		hash := model.ComputeTextureId(skinImage)
 | 
			
		||||
		oldHash, oldExist := profile.Textures[textureType]
 | 
			
		||||
		texture := model.Texture{}
 | 
			
		||||
		if err := tx.First(&texture, "hash = ?", hash).Error; err != nil {
 | 
			
		||||
			texture.Hash = hash
 | 
			
		||||
			texture.Used = 1
 | 
			
		||||
			buffer := bytes.Buffer{}
 | 
			
		||||
			err := png.Encode(&buffer, skinImage)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			texture.Data = buffer.Bytes()
 | 
			
		||||
			if err := tx.Create(&texture).Error; err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if oldExist && oldHash != hash {
 | 
			
		||||
				tx.Model(&texture).Update("used", gorm.Expr("used + ?", 1))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if oldExist && oldHash != hash {
 | 
			
		||||
			oldTexture := model.Texture{}
 | 
			
		||||
			if err := tx.Select("hash", "used").First(&oldTexture, "hash = ?", oldHash).Error; err == nil {
 | 
			
		||||
				if oldTexture.Used < 2 {
 | 
			
		||||
					tx.Delete(&oldTexture)
 | 
			
		||||
				} else {
 | 
			
		||||
					tx.Model(&oldTexture).Update("used", gorm.Expr("used - ?", 1))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		profile.Textures[textureType] = hash
 | 
			
		||||
		user.SetProfile(profile)
 | 
			
		||||
		return tx.Save(&user).Error
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								service/token_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								service/token_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU Affero General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
	"yggdrasil-go/model"
 | 
			
		||||
	"yggdrasil-go/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TokenService interface {
 | 
			
		||||
	RemoveToken(token *model.Token)
 | 
			
		||||
	RemoveAccessToken(accessToken string)
 | 
			
		||||
	RemoveAll(profileId uuid.UUID)
 | 
			
		||||
	AcquireToken(user *model.User, clientToken *string, profile *model.Profile) *model.Token
 | 
			
		||||
	VerifyToken(accessToken string, clientToken *string) model.AvailableLevel
 | 
			
		||||
	GetToken(accessToken string) (*model.Token, bool)
 | 
			
		||||
	UpdateProfile(profileId uuid.UUID, profile *model.Profile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type tokenStore struct {
 | 
			
		||||
	tokenCache *lru.Cache
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTokenService() TokenService {
 | 
			
		||||
	cache, _ := lru.New(10000000)
 | 
			
		||||
	store := tokenStore{
 | 
			
		||||
		tokenCache: cache,
 | 
			
		||||
	}
 | 
			
		||||
	return &store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tokenStore) RemoveToken(token *model.Token) {
 | 
			
		||||
	t.RemoveAccessToken(token.AccessToken)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tokenStore) RemoveAccessToken(accessToken string) {
 | 
			
		||||
	t.tokenCache.Remove(accessToken)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tokenStore) RemoveAll(profileId uuid.UUID) {
 | 
			
		||||
	keys := t.tokenCache.Keys()
 | 
			
		||||
	for _, k := range keys {
 | 
			
		||||
		if v, ok := t.tokenCache.Get(k); ok {
 | 
			
		||||
			if v.(*model.Token).SelectedProfile.Id == profileId {
 | 
			
		||||
				t.tokenCache.Remove(k)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tokenStore) AcquireToken(user *model.User, clientToken *string, profile *model.Profile) *model.Token {
 | 
			
		||||
	if profile == nil {
 | 
			
		||||
		var err error
 | 
			
		||||
		profile, err = user.Profile()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	token := model.NewToken(util.RandomUUID(), clientToken, profile)
 | 
			
		||||
	t.tokenCache.Add(token.AccessToken, &token)
 | 
			
		||||
	return &token
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tokenStore) VerifyToken(accessToken string, clientToken *string) model.AvailableLevel {
 | 
			
		||||
	if value, ok := t.tokenCache.Get(accessToken); ok {
 | 
			
		||||
		if token, ok := value.(*model.Token); ok {
 | 
			
		||||
			if clientToken != nil && token.ClientToken != *clientToken {
 | 
			
		||||
				return model.Invalid
 | 
			
		||||
			}
 | 
			
		||||
			if token.GetAvailableLevel() == model.Invalid {
 | 
			
		||||
				t.RemoveToken(token)
 | 
			
		||||
			}
 | 
			
		||||
			return token.GetAvailableLevel()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return model.Invalid
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tokenStore) GetToken(accessToken string) (*model.Token, bool) {
 | 
			
		||||
	if value, ok := t.tokenCache.Get(accessToken); ok {
 | 
			
		||||
		if token, ok := value.(*model.Token); ok {
 | 
			
		||||
			return token, true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tokenStore) UpdateProfile(profileId uuid.UUID, profile *model.Profile) {
 | 
			
		||||
	keys := t.tokenCache.Keys()
 | 
			
		||||
	for _, k := range keys {
 | 
			
		||||
		if v, ok := t.tokenCache.Get(k); ok {
 | 
			
		||||
			if token := v.(*model.Token); token.SelectedProfile.Id == profileId {
 | 
			
		||||
				token.SelectedProfile = *profile
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										412
									
								
								service/user_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										412
									
								
								service/user_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,412 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU Affero General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	lru "github.com/hashicorp/golang-lru"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
	"golang.org/x/time/rate"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"yggdrasil-go/model"
 | 
			
		||||
	"yggdrasil-go/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UserService interface {
 | 
			
		||||
	Register(username string, password string, profileName string) (*model.UserResponse, error)
 | 
			
		||||
	Login(username string, password string, clientToken *string, requestUser bool) (*LoginResponse, error)
 | 
			
		||||
	ChangeProfile(accessToken string, clientToken *string, changeTo string) error
 | 
			
		||||
	Refresh(accessToken string, clientToken *string, requestUser bool, selectedProfile *model.ProfileResponse) (*LoginResponse, error)
 | 
			
		||||
	Validate(accessToken string, clientToken *string) error
 | 
			
		||||
	Invalidate(accessToken string) error
 | 
			
		||||
	Signout(username string, password string) error
 | 
			
		||||
	UsernameToUUID(username string) (*model.ProfileResponse, error)
 | 
			
		||||
	QueryUUIDs(usernames []string) ([]model.ProfileResponse, error)
 | 
			
		||||
	QueryProfile(profileId uuid.UUID, unsigned bool, textureBaseUrl string) (map[string]interface{}, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LoginResponse struct {
 | 
			
		||||
	User              *model.UserResponse     `json:"user"`
 | 
			
		||||
	ClientToken       string                  `json:"clientToken"`
 | 
			
		||||
	AccessToken       string                  `json:"accessToken"`
 | 
			
		||||
	AvailableProfiles []model.ProfileResponse `json:"availableProfiles,omitempty"`
 | 
			
		||||
	SelectedProfile   *model.ProfileResponse  `json:"selectedProfile"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type userSrviceImpl struct {
 | 
			
		||||
	tokenService  TokenService
 | 
			
		||||
	db            *gorm.DB
 | 
			
		||||
	limitLruCache *lru.Cache
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUserService(tokenService TokenService, db *gorm.DB) UserService {
 | 
			
		||||
	cache, _ := lru.New(10000)
 | 
			
		||||
	userSrvice := userSrviceImpl{
 | 
			
		||||
		tokenService:  tokenService,
 | 
			
		||||
		db:            db,
 | 
			
		||||
		limitLruCache: cache,
 | 
			
		||||
	}
 | 
			
		||||
	return &userSrvice
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) Register(username string, password string, profileName string) (*model.UserResponse, error) {
 | 
			
		||||
	var count int64
 | 
			
		||||
	if err := u.db.Table("users").Where("email = ?", username).Count(&count).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		return nil, util.NewForbiddenOperationError("email exist")
 | 
			
		||||
	}
 | 
			
		||||
	if err := u.db.Table("users").Where("profile_name = ?", profileName).Count(&count).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		return nil, util.NewForbiddenOperationError("profileName exist")
 | 
			
		||||
	} else if _, err := mojangUsernameToUUID(profileName); err == nil {
 | 
			
		||||
		return nil, util.NewForbiddenOperationError("profileName duplicate")
 | 
			
		||||
	}
 | 
			
		||||
	matched, err := regexp.MatchString("^(\\w){3,}(\\.\\w+)*@(\\w){2,}((\\.\\w+)+)$", username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if !matched || len(password) < 6 || isInvalidProfileName(profileName) {
 | 
			
		||||
		return nil, util.NewIllegalArgumentError("bad format(valid email, password longer than 5, profileName longer than 1)")
 | 
			
		||||
	}
 | 
			
		||||
	hashedPass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{
 | 
			
		||||
		ID:       uuid.New(),
 | 
			
		||||
		Email:    username,
 | 
			
		||||
		Password: string(hashedPass),
 | 
			
		||||
	}
 | 
			
		||||
	profile := model.NewProfile(user.ID, profileName, model.STEVE, "")
 | 
			
		||||
	user.SetProfile(&profile)
 | 
			
		||||
 | 
			
		||||
	if err := u.db.Create(&user).Error; err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	response := user.ToResponse()
 | 
			
		||||
	return &response, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isInvalidProfileName(name string) bool {
 | 
			
		||||
	// To support Unicode (like Chinese) profile name, abandoned treatment.
 | 
			
		||||
	return name == "" || strings.ContainsRune(name, ' ') || len(name) <= 1
 | 
			
		||||
	//return name == "" || !name.matches("^[0-1a-zA-Z_]{2,16}$");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) Login(username string, password string, clientToken *string, requestUser bool) (*LoginResponse, error) {
 | 
			
		||||
	if !u.allowUser(username) {
 | 
			
		||||
		return nil, util.YggdrasilError{
 | 
			
		||||
			Status:       http.StatusTooManyRequests,
 | 
			
		||||
			ErrorCode:    "ForbiddenOperationException",
 | 
			
		||||
			ErrorMessage: "Forbidden",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	if err := u.db.Where("email = ?", username).First(&user).Error; err == nil {
 | 
			
		||||
		if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil {
 | 
			
		||||
			var useClientToken string
 | 
			
		||||
			if clientToken == nil || *clientToken == "" {
 | 
			
		||||
				useClientToken = util.RandomUUID()
 | 
			
		||||
			} else {
 | 
			
		||||
				useClientToken = *clientToken
 | 
			
		||||
			}
 | 
			
		||||
			token := u.tokenService.AcquireToken(&user, &useClientToken, nil)
 | 
			
		||||
			profile, err := user.Profile()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				panic(err)
 | 
			
		||||
			}
 | 
			
		||||
			simpleResponse := profile.ToSimpleResponse()
 | 
			
		||||
			var response = LoginResponse{
 | 
			
		||||
				AccessToken:       token.AccessToken,
 | 
			
		||||
				ClientToken:       token.ClientToken,
 | 
			
		||||
				AvailableProfiles: []model.ProfileResponse{simpleResponse},
 | 
			
		||||
				SelectedProfile:   &simpleResponse,
 | 
			
		||||
			}
 | 
			
		||||
			userResponse := user.ToResponse()
 | 
			
		||||
			if requestUser {
 | 
			
		||||
				response.User = &userResponse
 | 
			
		||||
			}
 | 
			
		||||
			return &response, nil
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil, util.NewForbiddenOperationError(util.MessageInvalidCredentials)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		data := map[string]interface{}{
 | 
			
		||||
			"agent": map[string]interface{}{
 | 
			
		||||
				"name":    "Minecraft",
 | 
			
		||||
				"version": 1,
 | 
			
		||||
			},
 | 
			
		||||
			"username":    username,
 | 
			
		||||
			"password":    password,
 | 
			
		||||
			"clientToken": password,
 | 
			
		||||
			"requestUser": requestUser,
 | 
			
		||||
		}
 | 
			
		||||
		loginResponse := LoginResponse{}
 | 
			
		||||
		err := util.PostObject("https://authserver.mojang.com/authenticate", data, &loginResponse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			return &loginResponse, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) ChangeProfile(accessToken string, clientToken *string, changeTo string) error {
 | 
			
		||||
	if u.tokenService.VerifyToken(accessToken, clientToken) != model.Valid {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
	}
 | 
			
		||||
	token, ok := u.tokenService.GetToken(accessToken)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	profile := token.SelectedProfile
 | 
			
		||||
	err := u.db.First(&user, profile.Id).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return util.NewForbiddenOperationError("User not found")
 | 
			
		||||
	}
 | 
			
		||||
	var count int64
 | 
			
		||||
	if err := u.db.Table("users").Where("profile_name = ?", changeTo).Count(&count).Error; err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		return util.NewForbiddenOperationError("profileName exist")
 | 
			
		||||
	} else if _, err := mojangUsernameToUUID(changeTo); err == nil {
 | 
			
		||||
		return util.NewForbiddenOperationError("profileName duplicate")
 | 
			
		||||
	}
 | 
			
		||||
	if isInvalidProfileName(changeTo) {
 | 
			
		||||
		return util.NewForbiddenOperationError("bad format(profileName longer than 1)")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = u.db.Model(&user).Update("profile_name", changeTo).Error; err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	profile.Name = changeTo
 | 
			
		||||
	u.tokenService.UpdateProfile(user.ID, &profile)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) Refresh(accessToken string, clientToken *string, requestUser bool, selectedProfile *model.ProfileResponse) (*LoginResponse, error) {
 | 
			
		||||
	if len(accessToken) <= 36 {
 | 
			
		||||
		user := model.User{}
 | 
			
		||||
		if selectedProfile != nil {
 | 
			
		||||
			// 由于当前实现把用户 UUID 作为角色 UUID,所以不支持角色选择,只要选择了就会报错
 | 
			
		||||
			return nil, util.NewForbiddenOperationError(util.MessageTokenAlreadyAssigned)
 | 
			
		||||
		}
 | 
			
		||||
		if u.tokenService.VerifyToken(accessToken, clientToken) == model.Invalid {
 | 
			
		||||
			return nil, util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
		}
 | 
			
		||||
		token, ok := u.tokenService.GetToken(accessToken)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return nil, util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := u.db.First(&user, token.SelectedProfile.Id).Error; err != nil {
 | 
			
		||||
			return nil, util.NewIllegalArgumentError(util.MessageProfileNotFound)
 | 
			
		||||
		}
 | 
			
		||||
		newToken := u.tokenService.AcquireToken(&user, clientToken, nil)
 | 
			
		||||
		u.tokenService.RemoveAccessToken(accessToken)
 | 
			
		||||
		simpleResponse := newToken.SelectedProfile.ToSimpleResponse()
 | 
			
		||||
		var response = LoginResponse{
 | 
			
		||||
			AccessToken:       newToken.AccessToken,
 | 
			
		||||
			ClientToken:       newToken.ClientToken,
 | 
			
		||||
			AvailableProfiles: []model.ProfileResponse{},
 | 
			
		||||
			SelectedProfile:   &simpleResponse,
 | 
			
		||||
		}
 | 
			
		||||
		userResponse := user.ToResponse()
 | 
			
		||||
		if requestUser {
 | 
			
		||||
			response.User = &userResponse
 | 
			
		||||
		}
 | 
			
		||||
		return &response, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		data := map[string]interface{}{
 | 
			
		||||
			"accessToken":     accessToken,
 | 
			
		||||
			"clientToken":     clientToken,
 | 
			
		||||
			"requestUser":     requestUser,
 | 
			
		||||
			"selectedProfile": selectedProfile,
 | 
			
		||||
		}
 | 
			
		||||
		loginResponse := LoginResponse{}
 | 
			
		||||
		err := util.PostObject("https://authserver.mojang.com/refresh", data, &loginResponse)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			return &loginResponse, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) Validate(accessToken string, clientToken *string) error {
 | 
			
		||||
	if len(accessToken) <= 36 {
 | 
			
		||||
		if u.tokenService.VerifyToken(accessToken, clientToken) != model.Valid {
 | 
			
		||||
			return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		data := map[string]interface{}{
 | 
			
		||||
			"accessToken": accessToken,
 | 
			
		||||
			"clientToken": clientToken,
 | 
			
		||||
		}
 | 
			
		||||
		err := util.PostObjectForError("https://authserver.mojang.com/validate", data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) Invalidate(accessToken string) error {
 | 
			
		||||
	if len(accessToken) <= 36 {
 | 
			
		||||
		u.tokenService.RemoveAccessToken(accessToken)
 | 
			
		||||
	} else {
 | 
			
		||||
		data := map[string]interface{}{
 | 
			
		||||
			"accessToken": accessToken,
 | 
			
		||||
		}
 | 
			
		||||
		err := util.PostObjectForError("https://authserver.mojang.com/invalidate", data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) Signout(username string, password string) error {
 | 
			
		||||
	if !u.allowUser(username) {
 | 
			
		||||
		return util.YggdrasilError{
 | 
			
		||||
			Status:       http.StatusTooManyRequests,
 | 
			
		||||
			ErrorCode:    "ForbiddenOperationException",
 | 
			
		||||
			ErrorMessage: "Forbidden",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	if err := u.db.Where("email = ?", username).First(&user).Error; err == nil {
 | 
			
		||||
		if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil {
 | 
			
		||||
			u.tokenService.RemoveAll(user.ID)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			return util.NewForbiddenOperationError(util.MessageInvalidCredentials)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		data := map[string]interface{}{
 | 
			
		||||
			"username": username,
 | 
			
		||||
			"password": password,
 | 
			
		||||
		}
 | 
			
		||||
		err := util.PostObjectForError("https://authserver.mojang.com/signout", data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) UsernameToUUID(username string) (*model.ProfileResponse, error) {
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	if result := u.db.Where("profile_name = ?", username).First(&user); result.Error == nil {
 | 
			
		||||
		return &model.ProfileResponse{
 | 
			
		||||
			Name: user.ProfileName,
 | 
			
		||||
			Id:   util.UnsignedString(user.ID),
 | 
			
		||||
		}, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		response, err := mojangUsernameToUUID(username)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil
 | 
			
		||||
		} else {
 | 
			
		||||
			return &response, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) QueryUUIDs(usernames []string) ([]model.ProfileResponse, error) {
 | 
			
		||||
	var users []model.User
 | 
			
		||||
	var names []string
 | 
			
		||||
	if len(usernames) > 10 {
 | 
			
		||||
		names = usernames[:10]
 | 
			
		||||
	} else {
 | 
			
		||||
		names = usernames
 | 
			
		||||
	}
 | 
			
		||||
	var responses = make([]model.ProfileResponse, 0)
 | 
			
		||||
	if err := u.db.Table("users").Where("profile_name in ?", names).Find(&users).Error; err == nil {
 | 
			
		||||
		for _, user := range users {
 | 
			
		||||
			responses = append(responses, model.ProfileResponse{
 | 
			
		||||
				Name: user.ProfileName,
 | 
			
		||||
				Id:   util.UnsignedString(user.ID),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return responses, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) QueryProfile(profileId uuid.UUID, unsigned bool, textureBaseUrl string) (map[string]interface{}, error) {
 | 
			
		||||
	user := model.User{}
 | 
			
		||||
	if err := u.db.First(&user, profileId).Error; err == nil {
 | 
			
		||||
		profile, err := user.Profile()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		response, err := profile.ToCompleteResponse(!unsigned, textureBaseUrl)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			return response, err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		result := map[string]interface{}{}
 | 
			
		||||
		err := util.GetObject(fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=%t", util.UnsignedString(profileId), unsigned), &result)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		} else {
 | 
			
		||||
			return result, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *userSrviceImpl) allowUser(username string) bool {
 | 
			
		||||
	if value, ok := u.limitLruCache.Get(username); ok {
 | 
			
		||||
		if limiter, ok := value.(*rate.Limiter); ok {
 | 
			
		||||
			return limiter.Allow()
 | 
			
		||||
		} else {
 | 
			
		||||
			u.limitLruCache.Remove(username)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		limiter := rate.NewLimiter(0.2, 3)
 | 
			
		||||
		u.limitLruCache.Add(username, limiter)
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mojangUsernameToUUID(username string) (model.ProfileResponse, error) {
 | 
			
		||||
	response := model.ProfileResponse{}
 | 
			
		||||
	reqUrl := fmt.Sprintf("https://api.mojang.com/users/profiles/minecraft/%s", url.PathEscape(username))
 | 
			
		||||
	err := util.GetObject(reqUrl, &response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return response, err
 | 
			
		||||
	} else {
 | 
			
		||||
		return response, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user