diff --git a/config/config.go b/config/config.go index 6e9a5b8..59134cf 100644 --- a/config/config.go +++ b/config/config.go @@ -15,8 +15,9 @@ type Config struct { Type string Ram int } - RaelIP bool - MaxIpUser int - RsaPriKey string - TexturePath string + RaelIP bool + MaxIpUser int + RsaPriKey string + TexturePath string + TextureBaseUrl string } diff --git a/db/ent/schema/userprofile.go b/db/ent/schema/userprofile.go index 9b3a937..da9869d 100644 --- a/db/ent/schema/userprofile.go +++ b/db/ent/schema/userprofile.go @@ -36,5 +36,7 @@ func (UserProfile) Edges() []ent.Edge { func (UserProfile) Indexes() []ent.Index { return []ent.Index{ index.Edges("user"), + index.Fields("name"), + index.Fields("uuid"), } } diff --git a/db/ent/schema/usertexture.go b/db/ent/schema/usertexture.go index 014e116..4ee5c07 100644 --- a/db/ent/schema/usertexture.go +++ b/db/ent/schema/usertexture.go @@ -18,11 +18,11 @@ func (UserTexture) Fields() []ent.Field { return []ent.Field{ field.Int("user_profile_id"), field.Int("texture_id"), - // 皮肤 or 披风 + // skin or cape field.String("type").SchemaType(map[string]string{ dialect.MySQL: "VARCHAR(10)", }), - // slim or 空 + // slim or "" field.String("variant").SchemaType(map[string]string{ dialect.MySQL: "VARCHAR(10)", }), diff --git a/handle/yggdrasil/user.go b/handle/yggdrasil/user.go index 49a2674..0c90966 100644 --- a/handle/yggdrasil/user.go +++ b/handle/yggdrasil/user.go @@ -119,3 +119,37 @@ func (y *Yggdrasil) Refresh() httprouter.Handle { w.Write(b) } } + +func (y *Yggdrasil) GetProfile() httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + ctx := r.Context() + uuid := p.ByName("uuid") + unsigned := r.FormValue("unsigned") + + unsignedBool := true + + switch unsigned { + case "true": + case "false": + unsignedBool = false + default: + y.logger.DebugContext(ctx, "unsigned 参数类型错误") + handleYgError(ctx, w, yggdrasil.Error{ErrorMessage: "unsigned 参数类型错误"}, 400) + return + + } + u, err := y.yggdrasilService.GetProfile(ctx, uuid, unsignedBool, r.Host) + if err != nil { + if errors.Is(err, yggdrasilS.ErrNotUser) { + y.logger.DebugContext(ctx, err.Error()) + w.WriteHeader(204) + return + } + y.logger.WarnContext(ctx, err.Error()) + handleYgError(ctx, w, yggdrasil.Error{ErrorMessage: err.Error()}, 500) + return + } + b, _ := json.Marshal(u) + w.Write(b) + } +} diff --git a/model/yggdrasil/model.go b/model/yggdrasil/model.go index a3cdf2c..1a8b333 100644 --- a/model/yggdrasil/model.go +++ b/model/yggdrasil/model.go @@ -52,3 +52,15 @@ type RefreshToken struct { RequestUser bool `json:"requestUser"` SelectedProfile TokenProfile `json:"selectedProfile"` } + +type UserInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Properties []UserProperties `json:"properties"` +} + +type UserProperties struct { + Name string `json:"name"` + Value string `json:"value"` + Signature string `json:"signature,omitempty"` +} diff --git a/model/yggdrasil/textures.go b/model/yggdrasil/textures.go new file mode 100644 index 0000000..d37c548 --- /dev/null +++ b/model/yggdrasil/textures.go @@ -0,0 +1,28 @@ +package yggdrasil + +import ( + "encoding/base64" + "encoding/json" +) + +type UserTextures struct { + // UUID + ProfileID string `json:"profileId"` + ProfileName string `json:"profileName"` + Textures map[string]Textures `json:"textures"` + // 时间戳 毫秒 + Timestamp string `json:"timestamp"` +} + +type Textures struct { + Url string `json:"url"` + Metadata map[string]string `json:"metadata"` +} + +func (u UserTextures) Base64() string { + b, err := json.Marshal(u) + if err != nil { + panic(err) + } + return base64.StdEncoding.EncodeToString(b) +} diff --git a/server/route/route.go b/server/route/route.go index c63b9d0..7db20eb 100644 --- a/server/route/route.go +++ b/server/route/route.go @@ -27,9 +27,13 @@ func newYggdrasil(r *httprouter.Router, handelY yggdrasil.Yggdrasil) error { r.POST("/api/yggdrasil/authserver/validate", warpHJSON(handelY.Validate())) r.POST("/api/yggdrasil/authserver/signout", warpHJSON(handelY.Signout())) r.POST("/api/yggdrasil/authserver/invalidate", handelY.Invalidate()) - r.POST("/api/yggdrasil/authserver/refresh", handelY.Refresh()) + r.POST("/api/yggdrasil/authserver/refresh", warpHJSON(handelY.Refresh())) + r.PUT("/api/yggdrasil/api/user/profile/:uuid/:textureType", handelY.PutTexture()) - r.DELETE("/api/yggdrasil/api/user/profile/:uuid/:textureType", handelY.DelTexture()) + r.DELETE("/api/yggdrasil/api/user/profile/:uuid/:textureType", warpHJSON(handelY.DelTexture())) + + r.GET("/sessionserver/session/minecraft/profile/:uuid", handelY.GetProfile()) + r.GET("/api/yggdrasil", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { w.Write([]byte(`{ "meta": { diff --git a/service/yggdrasil/user.go b/service/yggdrasil/user.go index f297b8d..7a9606d 100644 --- a/service/yggdrasil/user.go +++ b/service/yggdrasil/user.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "path" "strconv" "strings" "time" @@ -16,11 +17,13 @@ import ( "github.com/xmdhs/authlib-skin/model/yggdrasil" sutils "github.com/xmdhs/authlib-skin/service/utils" "github.com/xmdhs/authlib-skin/utils" + "github.com/xmdhs/authlib-skin/utils/sign" ) var ( ErrRate = errors.New("频率限制") ErrPassWord = errors.New("错误的密码或邮箱") + ErrNotUser = errors.New("没有这个用户") ) func (y *Yggdrasil) validatePass(cxt context.Context, email, pass string) (*ent.User, error) { @@ -164,3 +167,62 @@ func (y *Yggdrasil) Refresh(ctx context.Context, token yggdrasil.RefreshToken) ( }, }, nil } + +func (y *Yggdrasil) GetProfile(ctx context.Context, uuid string, unsigned bool, host string) (yggdrasil.UserInfo, error) { + up, err := y.client.UserProfile.Query().Where(userprofile.UUID(uuid)).WithUsertexture().WithTexture().Only(ctx) + if err != nil { + var nf *ent.NotFoundError + if errors.As(err, &nf) { + return yggdrasil.UserInfo{}, fmt.Errorf("GetProfile: %w", ErrNotUser) + } + return yggdrasil.UserInfo{}, fmt.Errorf("GetProfile: %w", err) + } + var baseURl string + if y.config.TextureBaseUrl == "" { + baseURl = path.Join(host, "textures") + } + + ut := yggdrasil.UserTextures{ + ProfileID: up.UUID, + ProfileName: up.Name, + Textures: map[string]yggdrasil.Textures{}, + Timestamp: strconv.FormatInt(time.Now().UnixMilli(), 10), + } + + for _, v := range up.Edges.Usertexture { + hashstr := v.Edges.Texture.TextureHash + t := yggdrasil.Textures{ + Url: path.Join(baseURl, hashstr[:2], hashstr[2:4], hashstr), + Metadata: map[string]string{}, + } + if v.Variant == "slim" { + t.Metadata["model"] = "slim" + } + ut.Textures[strings.ToTitle(v.Type)] = t + } + + texturesBase64 := ut.Base64() + + var signStr string + if !unsigned { + s := sign.NewAuthlibSignWithKey(y.prikey) + signStr, err = s.Sign([]byte(texturesBase64)) + if err != nil { + return yggdrasil.UserInfo{}, fmt.Errorf("GetProfile: %w", err) + } + } + + uinfo := yggdrasil.UserInfo{ + ID: up.UUID, + Name: up.Name, + Properties: []yggdrasil.UserProperties{ + { + Name: "textures", + Value: texturesBase64, + Signature: signStr, + }, + }, + } + + return uinfo, nil +}