原始提交
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