From f786e9d339ff5b45c202e77ff8d0ed34152468bf Mon Sep 17 00:00:00 2001 From: a3510377 Date: Wed, 1 May 2024 22:09:50 +0800 Subject: [PATCH 1/3] Update member.go --- server/api/member.go | 65 +++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/server/api/member.go b/server/api/member.go index cfd5e3d..cfa7206 100644 --- a/server/api/member.go +++ b/server/api/member.go @@ -115,6 +115,26 @@ type OnlineUUIDStruct struct { Name string `json:"name"` } +func getOne(name string) (onlineUUID *OnlineUUIDStruct, err error) { + resp, err := http.Get("https://api.mojang.com/users/profiles/minecraft/" + name) + if err != nil { + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + fmt.Println(string(body)) + if err = json.Unmarshal(body, onlineUUID); err != nil { + return + } + + return +} + func fetchOnlineUUIDs(names []string) map[string]string { result := map[string]string{} var wg sync.WaitGroup @@ -128,39 +148,22 @@ func fetchOnlineUUIDs(names []string) map[string]string { } wg.Add(1) - getOne := func(name string) error { - resp, err := http.Get("https://api.mojang.com/users/profiles/minecraft/" + name) - if err != nil { - return err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - onlineUUID := OnlineUUIDStruct{} - if err := json.Unmarshal(body, &onlineUUID); err != nil { - return err - } - - mu.Lock() - defer mu.Unlock() - if onlineUUID.ID != name { - result[name] = DEFAULT_UUID - } - result[onlineUUID.Name] = onlineUUID.ID - - return nil - } - getOnes := func(name ...string) { - for _, n := range name { - if err := getOne(n); err != nil { + getOnes := func(names ...string) { + for _, name := range names { + if onlineUUID, err := getOne(name); err != nil { + if errors.Is(err, os.ErrNotExist) { + log.WithError(err).Error(fmt.Sprintf("Failed to find online UUID of %s", name)) + continue + } + log.WithError(err).Error(fmt.Sprintf("Failed to get the UUID of %s", name)) + } else { mu.Lock() - result[n] = DEFAULT_UUID + if onlineUUID.ID != name { + result[name] = DEFAULT_UUID + log.Warn(fmt.Sprintf("Member ID error, get %s but real is %s", name, onlineUUID.ID)) + } + result[onlineUUID.Name] = onlineUUID.ID mu.Unlock() - log.WithError(err).Error(fmt.Sprintf("Failed to get the UUID of %s", n)) } } } From 0ae112e290027727b01341be6676dfa45a117291 Mon Sep 17 00:00:00 2001 From: a3510377 Date: Fri, 3 May 2024 22:27:05 +0800 Subject: [PATCH 2/3] feat utils minecraft --- server/go.mod | 1 + server/go.sum | 2 + server/utils/minecraft.go | 148 +++++++++++++++++++++++++++++++++ server/utils/minecraft_test.go | 68 +++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 server/utils/minecraft.go create mode 100644 server/utils/minecraft_test.go diff --git a/server/go.mod b/server/go.mod index 5ae9cc0..ea0c822 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,6 +5,7 @@ go 1.22.0 toolchain go1.22.2 require ( + github.com/deckarep/golang-set/v2 v2.6.0 github.com/gin-gonic/gin v1.9.1 github.com/mattn/go-colorable v0.1.13 github.com/sirupsen/logrus v1.9.3 diff --git a/server/go.sum b/server/go.sum index 23d667d..875f59b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -11,6 +11,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs= diff --git a/server/utils/minecraft.go b/server/utils/minecraft.go new file mode 100644 index 0000000..c69ad4e --- /dev/null +++ b/server/utils/minecraft.go @@ -0,0 +1,148 @@ +package utils + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" +) + +type OnlineUUIDStruct struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func GetMinecraftInfoFromName(name string) (*OnlineUUIDStruct, error) { + resp, err := http.Get("https://api.mojang.com/users/profiles/minecraft/" + name) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + onlineUUID := OnlineUUIDStruct{} + if err = json.Unmarshal(body, &onlineUUID); err != nil { + return nil, err + } + + return &onlineUUID, nil +} + +func getMinecraftInfosFrom10Names(names []string) (*[]OnlineUUIDStruct, error) { + size := len(names) + if size > 10 { + return nil, errors.New("Too many names") + } + + if size == 0 { + return &[]OnlineUUIDStruct{}, nil + } + + nameString, err := json.Marshal(names) + if err != nil { + return nil, err + } + + resp, err := http.Post("https://api.mojang.com/profiles/minecraft", "application/json", bytes.NewBuffer(nameString)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := []OnlineUUIDStruct{} + if err = json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func GetMinecraftInfosFromNames(names ...string) ([]*OnlineUUIDStruct, error) { + results := []OnlineUUIDStruct{} + max := len(names) + for i := 0; i < max; i += 10 { + end := i + 10 + if end > max { + end = max + } + + sliceNames := names[i:end] + result, err := getMinecraftInfosFrom10Names(sliceNames) + if err != nil { + continue + } + + results = append(results, *result...) + } + // if err != nil { + // getOnes(sliceNames...) + // return + // } + // defer resp.Body.Close() + + // body, err := io.ReadAll(resp.Body) + // if err != nil { + // getOnes(sliceNames...) + // return + // } + + // onlineUUID := []OnlineUUIDStruct{} + // if err := json.Unmarshal(body, &onlineUUID); err != nil { + // getOnes(sliceNames...) + // return + // } + return nil, nil + + // go func(start, end int) { + // defer wg.Done() + + // sliceNames := names[start:end] + // nameString, err := json.Marshal(sliceNames) + // if err != nil { + // getOnes(sliceNames...) + // return + // } + + // resp, err := http.Post("https://api.mojang.com/profiles/minecraft", "application/json", bytes.NewBuffer(nameString)) + // if err != nil { + // getOnes(sliceNames...) + // return + // } + // defer resp.Body.Close() + + // body, err := io.ReadAll(resp.Body) + // if err != nil { + // getOnes(sliceNames...) + // return + // } + + // onlineUUID := []OnlineUUIDStruct{} + // if err := json.Unmarshal(body, &onlineUUID); err != nil { + // getOnes(sliceNames...) + // return + // } + + // // Check if the length is equal + // if len(onlineUUID) != len(sliceNames) { + // getOnes(sliceNames...) + // return + // } + + // mu.Lock() + // defer mu.Unlock() + // for _, u := range onlineUUID { + // result[u.Name] = u.ID + // } + // }(i, end) + +} diff --git a/server/utils/minecraft_test.go b/server/utils/minecraft_test.go new file mode 100644 index 0000000..76f1ab4 --- /dev/null +++ b/server/utils/minecraft_test.go @@ -0,0 +1,68 @@ +package utils + +import ( + "fmt" + "testing" + + mapset "github.com/deckarep/golang-set/v2" +) + +func TestGetMinecraftInfoFromName(t *testing.T) { + for _, name := range []string{"Steve", "StevE"} { + info, err := GetMinecraftInfoFromName(name) + if err != nil { + t.Error(err) + continue + } + + if info == nil { + t.Errorf("Expected info, got nil") + continue + } + + if info.Name != "Steve" { + t.Errorf("Expected name Steve, got %s", info.Name) + } else if info.ID != "8667ba71b85a4004af54457a9734eed7" { + t.Errorf("Expected 8667ba71b85a4004af54457a9734eed7, got %s", info.ID) + } + } +} + +func TestGetMinecraftInfosFrom10Names(t *testing.T) { + names := []string{ + "Steve", "Alex", "Noor", "Sunny", "Ari", + "Zuri", "Makena", "Kai", "Efe", + } + + infos, err := getMinecraftInfosFrom10Names(names) + if err != nil { + t.Error(err) + return + } + + players := mapset.NewSet( + OnlineUUIDStruct{ID: "ec561538f3fd461daff5086b22154bce", Name: "Alex"}, + OnlineUUIDStruct{ID: "938e960d50ab489b9b2aaf3751942989", Name: "Ari"}, + OnlineUUIDStruct{ID: "20bf454f34e34010a378613546e3d0f9", Name: "efe"}, + OnlineUUIDStruct{ID: "cf9858b6ed4946538e47f0e4214539f7", Name: "Kai"}, + OnlineUUIDStruct{ID: "6c4bc87ce82944efa1ad63d45e2b9545", Name: "Makena"}, + OnlineUUIDStruct{ID: "2d9f2227592b481d8433d13b69473ccc", Name: "noor"}, + OnlineUUIDStruct{ID: "8667ba71b85a4004af54457a9734eed7", Name: "Steve"}, + OnlineUUIDStruct{ID: "bafbe1cb77b348099fa3c89604bda644", Name: "Sunny"}, + OnlineUUIDStruct{ID: "f5e039b93b8a45109ee8e7552e098c55", Name: "Zuri"}, + ) + fmt.Println(players.Difference(mapset.NewSet(*infos...))) + + // waitMatchData := mapset.NewSet(*infos...) + // waitMatchData.Difference() + // for _, player := range players { + // if waitMatchData.Contains(player) { + // waitMatchData.Remove(player) + // } else { + // t.Errorf("Expected %v, got nil", player) + // } + // } + // if waitMatchData.Cardinality() != 0 { + // t.Errorf("Expected 0, got %d", waitMatchData.Cardinality()) + // } +} From bfa8e947f0f14c85ed03ee91372ab26d72a13fb6 Mon Sep 17 00:00:00 2001 From: a3510377 Date: Sat, 4 May 2024 07:51:52 +0800 Subject: [PATCH 3/3] feat jwt --- server/api/manage.go | 1 + server/config/config.go | 1 + server/config/config.yaml | 1 + server/config/jwt.go | 62 +++++++++++++++++++++++++ server/go.mod | 1 + server/go.sum | 2 + server/model/user.go | 1 + server/pkg/auth/jwt.go | 80 +++++++++++++++++++++++++++++++++ server/pkg/database/database.go | 16 +++++++ 9 files changed, 165 insertions(+) create mode 100644 server/api/manage.go create mode 100644 server/config/jwt.go create mode 100644 server/model/user.go create mode 100644 server/pkg/auth/jwt.go create mode 100644 server/pkg/database/database.go diff --git a/server/api/manage.go b/server/api/manage.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/server/api/manage.go @@ -0,0 +1 @@ +package api diff --git a/server/config/config.go b/server/config/config.go index 5b035fc..ed9e43d 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -21,6 +21,7 @@ type Config struct { Address string `yaml:"address"` AllowOrigins []string `yaml:"allow_origins"` MemberFile string `yaml:"member_file"` + JwtExpireDay int `yaml:"jwt_expire_day"` } `yaml:"api"` Cache struct { Root string `yaml:"root"` diff --git a/server/config/config.yaml b/server/config/config.yaml index 6ca630f..f94998b 100644 --- a/server/config/config.yaml +++ b/server/config/config.yaml @@ -3,6 +3,7 @@ api: allow_origins: - '*' member_file: data/data/members.yaml + jwt_expire_day: 7 cache: root: ./data/api/cache uuid_file: uuid.json diff --git a/server/config/jwt.go b/server/config/jwt.go new file mode 100644 index 0000000..0905356 --- /dev/null +++ b/server/config/jwt.go @@ -0,0 +1,62 @@ +package config + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" +) + +var ( + jwtPublicKey *ecdsa.PublicKey + jwtPrivateKey *ecdsa.PrivateKey +) + +func GetJwtPrivateKey() *ecdsa.PrivateKey { + return jwtPrivateKey +} + +func GetJwtPublicKey() *ecdsa.PublicKey { + return jwtPublicKey +} + +func LoadPrivateKey(path string) error { + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read private key file: %w", err) + } + + block, _ := pem.Decode(content) + if block == nil { + return fmt.Errorf("failed to decode private key") + } + + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + + jwtPrivateKey = key + return nil +} + +func LoadPublicKey(path string) error { + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read public key file: %w", err) + } + + block, _ := pem.Decode(content) + if block == nil { + return fmt.Errorf("failed to decode public key") + } + + key, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse public key: %w", err) + } + + jwtPublicKey = key.(*ecdsa.PublicKey) + return nil +} diff --git a/server/go.mod b/server/go.mod index ea0c822..aafa471 100644 --- a/server/go.mod +++ b/server/go.mod @@ -7,6 +7,7 @@ toolchain go1.22.2 require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/mattn/go-colorable v0.1.13 github.com/sirupsen/logrus v1.9.3 gopkg.in/yaml.v2 v2.4.0 diff --git a/server/go.sum b/server/go.sum index 875f59b..08459a7 100644 --- a/server/go.sum +++ b/server/go.sum @@ -31,6 +31,8 @@ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/server/model/user.go b/server/model/user.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/server/model/user.go @@ -0,0 +1 @@ +package model diff --git a/server/pkg/auth/jwt.go b/server/pkg/auth/jwt.go new file mode 100644 index 0000000..cdf1299 --- /dev/null +++ b/server/pkg/auth/jwt.go @@ -0,0 +1,80 @@ +package auth + +import ( + "errors" + "time" + + "server/config" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +const JWT_ISSUER = "ctec-api-server" + +var ErrTokenValidation = errors.New("token validation failed") + +type JwtCustomClaims struct { + jwt.RegisteredClaims + ID uint `json:"id"` +} + +type JwtToken struct { + Token string `json:"token"` + Claims JwtCustomClaims `json:"claims"` +} + +func New() *JwtToken { + return &JwtToken{} +} + +// CreateUserToken creates a token for the user +func CreateUserToken(ID uint) (*JwtToken, error) { + expiresAt := time.Now().Add(time.Hour * 24 * time.Duration(config.Get().API.JwtExpireDay)) + claims := JwtCustomClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: JWT_ISSUER, + ExpiresAt: jwt.NewNumericDate(expiresAt), + }, + ID: ID, + } + + t := jwt.New(jwt.SigningMethodES256) + t.Claims = &claims + tokenString, err := t.SignedString(config.GetJwtPrivateKey()) + if err != nil { + return nil, err + } + + return &JwtToken{Token: tokenString, Claims: claims}, nil +} + +// ParseToken parses the token and returns the claims +func (j *JwtToken) ParseToken(token string) (*JwtCustomClaims, error) { + t, err := jwt.ParseWithClaims(token, &JwtCustomClaims{}, func(t *jwt.Token) (any, error) { + return config.GetJwtPublicKey(), nil + }) + if err != nil { + return nil, err + } + + claims, ok := t.Claims.(*JwtCustomClaims) + if !ok || !t.Valid { + return nil, ErrTokenValidation + } + + return claims, nil +} + +// JwtClaims returns the claims from the token +func (j *JwtToken) JwtClaims(c *gin.Context) (*JwtCustomClaims, error) { + token := c.GetHeader("Authorization") + claims, err := j.ParseToken(token) + return claims, err +} + +// JwtUserId returns the user ID from the token +func (j *JwtToken) JwtUserId(c *gin.Context) uint { + claims, _ := j.JwtClaims(c) + return claims.ID +} diff --git a/server/pkg/database/database.go b/server/pkg/database/database.go new file mode 100644 index 0000000..2713b2f --- /dev/null +++ b/server/pkg/database/database.go @@ -0,0 +1,16 @@ +package database + +import ( + "strconv" + "time" +) + +type Model struct { + ID uint `gorm:"primarykey" json:"id,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + +func (m *Model) IDtoString() string { + return strconv.Itoa(int(m.ID)) +}