From 07903cf9c8fecf5329ee09731e5af48ec894cd99 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 30 Jun 2017 18:40:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D0=B0=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D1=8B=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/root.go | 75 ++++++++++ cmd/serve.go | 78 ++++++++++ daemon/http.go | 54 +++++++ db/capes/config.go | 7 + db/capes/files/db.go | 11 ++ db/capes/files/errors.go | 11 ++ db/capes/files/repository.go | 26 ++++ db/skins/config.go | 7 + db/skins/redis/commands.go | 58 ++++++++ db/skins/redis/db.go | 23 +++ db/skins/redis/errors.go | 12 ++ lib/tools/zlib.go => db/skins/redis/tools.go | 13 +- glide.lock | 59 +++++++- glide.yaml | 5 + lib/data/CapeItem.go | 43 ------ lib/data/SignedTexturesResponse.go | 14 -- lib/data/SkinItem.go | 117 --------------- lib/data/TexturesResponse.go | 21 --- lib/external/accounts/AccountInfo.go | 44 ------ lib/external/accounts/GetToken.go | 49 ------- lib/external/accounts/base.go | 51 ------- lib/routes/Cape.go | 40 ----- lib/routes/Face.go | 29 ---- lib/routes/MinecraftPHP.go | 34 ----- lib/routes/NotFound.go | 18 --- lib/routes/SignedTextures.go | 44 ------ lib/routes/Skin.go | 39 ----- lib/routes/Textures.go | 61 -------- lib/services/services.go | 18 --- lib/tools/tools_test.go | 32 ---- lib/worker/handlers.go | 82 ----------- lib/worker/supports.go | 60 -------- lib/worker/worker.go | 88 ----------- main.go | 7 + minecraft-skinsystem.go | 147 ------------------- model/cape.go | 11 ++ lib/worker/models.go => model/skin.go | 24 +-- ui/cape.go | 39 +++++ ui/face.go | 28 ++++ ui/minecraft_php.go | 33 +++++ ui/not_found.go | 18 +++ ui/service.go | 25 ++++ ui/signed_textures.go | 55 +++++++ ui/skin.go | 38 +++++ ui/textures.go | 92 ++++++++++++ ui/ui.go | 39 +++++ lib/tools/tools.go => utils/utils.go | 16 +- utils/utils_test.go | 60 ++++++++ 48 files changed, 894 insertions(+), 1061 deletions(-) create mode 100644 cmd/root.go create mode 100644 cmd/serve.go create mode 100644 daemon/http.go create mode 100644 db/capes/config.go create mode 100644 db/capes/files/db.go create mode 100644 db/capes/files/errors.go create mode 100644 db/capes/files/repository.go create mode 100644 db/skins/config.go create mode 100644 db/skins/redis/commands.go create mode 100644 db/skins/redis/db.go create mode 100644 db/skins/redis/errors.go rename lib/tools/zlib.go => db/skins/redis/tools.go (66%) delete mode 100644 lib/data/CapeItem.go delete mode 100644 lib/data/SignedTexturesResponse.go delete mode 100644 lib/data/SkinItem.go delete mode 100644 lib/data/TexturesResponse.go delete mode 100644 lib/external/accounts/AccountInfo.go delete mode 100644 lib/external/accounts/GetToken.go delete mode 100644 lib/external/accounts/base.go delete mode 100644 lib/routes/Cape.go delete mode 100644 lib/routes/Face.go delete mode 100644 lib/routes/MinecraftPHP.go delete mode 100644 lib/routes/NotFound.go delete mode 100644 lib/routes/SignedTextures.go delete mode 100644 lib/routes/Skin.go delete mode 100644 lib/routes/Textures.go delete mode 100644 lib/services/services.go delete mode 100644 lib/tools/tools_test.go delete mode 100644 lib/worker/handlers.go delete mode 100644 lib/worker/supports.go delete mode 100644 lib/worker/worker.go create mode 100644 main.go delete mode 100644 minecraft-skinsystem.go create mode 100644 model/cape.go rename lib/worker/models.go => model/skin.go (54%) create mode 100644 ui/cape.go create mode 100644 ui/face.go create mode 100644 ui/minecraft_php.go create mode 100644 ui/not_found.go create mode 100644 ui/service.go create mode 100644 ui/signed_textures.go create mode 100644 ui/skin.go create mode 100644 ui/textures.go create mode 100644 ui/ui.go rename lib/tools/tools.go => utils/utils.go (86%) create mode 100644 utils/utils_test.go diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..9171a1d --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "test", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Search config in home directory with name ".test" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".test") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..cff9bb6 --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "elyby/minecraft-skinsystem/daemon" + "elyby/minecraft-skinsystem/ui" + + "elyby/minecraft-skinsystem/db/skins/redis" + + "path" + "path/filepath" + "runtime" + + "elyby/minecraft-skinsystem/db/capes/files" + + "fmt" + + "github.com/mono83/slf/rays" + "github.com/mono83/slf/recievers/ansi" + "github.com/mono83/slf/wd" + "github.com/spf13/cobra" +) + +// serveCmd represents the serve command +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Запускает сервер системы скинов", + Long: "Более длинное описание пока не было придумано", + Run: func(cmd *cobra.Command, args []string) { + // TODO: извлечь все инициализации зависимостей в парсер конфигурации + + // Logger + wd.AddReceiver(ansi.New(true, true, false)) + logger := wd.New("", "").WithParams(rays.Host) + + // Skins repository + logger.Info("Connecting to redis") + skinsRepoCfg := &redis.Config{ + //Addr: "redis:6379", + Addr: "localhost:16379", + PollSize: 10, + } + skinsRepo, err := skinsRepoCfg.CreateRepo() + if err != nil { + logger.Emergency(fmt.Sprintf("Error on creating skins repo: %v", err)) + return + } + logger.Info("Successfully connected to redis") + + // Capes repository + _, file, _, _ := runtime.Caller(0) + capesRepoCfg := &files.Config{ + StoragePath: path.Join(filepath.Dir(file), "data/capes"), + } + capesRepo, err := capesRepoCfg.CreateRepo() + if err != nil { + logger.Emergency(fmt.Sprintf("Error on creating capes repo: %v", err)) + return + } + + + + cfg := &daemon.Config{ + ListenSpec: "localhost:35644", + SkinsRepo: skinsRepo, + CapesRepo: capesRepo, + Logger: logger, + UI: ui.Config{}, + } + + if err := daemon.Run(cfg); err != nil { + logger.Error(fmt.Sprintf("Error in main(): %v", err)) + } + }, +} + +func init() { + RootCmd.AddCommand(serveCmd) +} diff --git a/daemon/http.go b/daemon/http.go new file mode 100644 index 0000000..b45d732 --- /dev/null +++ b/daemon/http.go @@ -0,0 +1,54 @@ +package daemon + +import ( + "net" + + "elyby/minecraft-skinsystem/model" + "elyby/minecraft-skinsystem/ui" + + "fmt" + + "os" + "os/signal" + "syscall" + + "github.com/mono83/slf/wd" +) + +type Config struct { + ListenSpec string + + SkinsRepo model.SkinsRepository + CapesRepo model.CapesRepository + Logger wd.Watchdog + UI ui.Config +} + +func Run(cfg *Config) error { + cfg.Logger.Info(fmt.Sprintf("Starting, HTTP on: %s\n", cfg.ListenSpec)) + + uiService, err := ui.NewUiService(cfg.Logger, cfg.SkinsRepo, cfg.CapesRepo) + if err != nil { + cfg.Logger.Error(fmt.Sprintf("Error creating ui services: %v\n", err)) + return err + } + + listener, err := net.Listen("tcp", cfg.ListenSpec) + if err != nil { + cfg.Logger.Error(fmt.Sprintf("Error creating listener: %v\n", err)) + return err + } + + ui.Start(cfg.UI, uiService, listener) + + waitForSignal(cfg) + + return nil +} + +func waitForSignal(cfg *Config) { + ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + s := <-ch + cfg.Logger.Info(fmt.Sprintf("Got signal: %v, exiting.", s)) +} diff --git a/db/capes/config.go b/db/capes/config.go new file mode 100644 index 0000000..141d7f0 --- /dev/null +++ b/db/capes/config.go @@ -0,0 +1,7 @@ +package capes + +import "elyby/minecraft-skinsystem/model" + +type CapesRepositoryConfig interface { + CreateRepo() (model.CapesRepository, error) +} diff --git a/db/capes/files/db.go b/db/capes/files/db.go new file mode 100644 index 0000000..eb4e058 --- /dev/null +++ b/db/capes/files/db.go @@ -0,0 +1,11 @@ +package files + +import "elyby/minecraft-skinsystem/model" + +type Config struct { + StoragePath string +} + +func (cfg *Config) CreateRepo() (model.CapesRepository, error) { + return &filesDb{path: cfg.StoragePath}, nil +} diff --git a/db/capes/files/errors.go b/db/capes/files/errors.go new file mode 100644 index 0000000..79aa91f --- /dev/null +++ b/db/capes/files/errors.go @@ -0,0 +1,11 @@ +package files + +import "fmt" + +type CapeNotFound struct { + Who string +} + +func (e CapeNotFound) Error() string { + return fmt.Sprintf("Cape file not found. Required username \"%v\"", e.Who) +} diff --git a/db/capes/files/repository.go b/db/capes/files/repository.go new file mode 100644 index 0000000..848037d --- /dev/null +++ b/db/capes/files/repository.go @@ -0,0 +1,26 @@ +package files + +import ( + "os" + "path" + "strings" + + "elyby/minecraft-skinsystem/model" +) + +type filesDb struct { + path string +} + +func (repository *filesDb) FindByUsername(username string) (model.Cape, error) { + var record model.Cape + capePath := path.Join(repository.path, strings.ToLower(username) + ".png") + file, err := os.Open(capePath) + if err != nil { + return record, CapeNotFound{username} + } + + record.File = file + + return record, nil +} diff --git a/db/skins/config.go b/db/skins/config.go new file mode 100644 index 0000000..757e5c3 --- /dev/null +++ b/db/skins/config.go @@ -0,0 +1,7 @@ +package skins + +import "elyby/minecraft-skinsystem/model" + +type SkinsRepositoryConfig interface { + CreateRepo() (model.SkinsRepository, error) +} diff --git a/db/skins/redis/commands.go b/db/skins/redis/commands.go new file mode 100644 index 0000000..629664a --- /dev/null +++ b/db/skins/redis/commands.go @@ -0,0 +1,58 @@ +package redis + +import ( + "elyby/minecraft-skinsystem/model" + + "encoding/json" + "log" + + "github.com/mediocregopher/radix.v2/redis" + "github.com/mediocregopher/radix.v2/util" +) + +type redisDb struct { + conn util.Cmder +} + +const accountIdToUsernameKey string = "hash:username-to-account-id" + +func (db *redisDb) FindByUsername(username string) (model.Skin, error) { + var record model.Skin + redisKey := buildKey(username) + response := db.conn.Cmd("GET", redisKey) + if response.IsType(redis.Nil) { + return record, SkinNotFound{username} + } + + encodedResult, err := response.Bytes() + if err == nil { + result, err := zlibDecode(encodedResult) + if err != nil { + log.Println("Cannot uncompress zlib for key " + redisKey) + goto finish + } + + err = json.Unmarshal(result, &record) + if err != nil { + log.Println("Cannot decode record data for key" + redisKey) + goto finish + } + + record.OldUsername = record.Username + } + + finish: + + return record, err +} + +func (db *redisDb) FindByUserId(id int) (model.Skin, error) { + response := db.conn.Cmd("HGET", accountIdToUsernameKey, id) + if response.IsType(redis.Nil) { + return model.Skin{}, SkinNotFound{"unknown"} + } + + username, _ := response.Str() + + return db.FindByUsername(username) +} diff --git a/db/skins/redis/db.go b/db/skins/redis/db.go new file mode 100644 index 0000000..765da18 --- /dev/null +++ b/db/skins/redis/db.go @@ -0,0 +1,23 @@ +package redis + +import ( + "elyby/minecraft-skinsystem/model" + + "github.com/mediocregopher/radix.v2/pool" +) + +type Config struct { + Addr string + PollSize int +} + +func (cfg *Config) CreateRepo() (model.SkinsRepository, error) { + conn, err := pool.New("tcp", cfg.Addr, cfg.PollSize) + if err != nil { + return nil, err + } + + // TODO: здесь можно запустить горутину по восстановлению соединения + + return &redisDb{conn: conn}, err +} diff --git a/db/skins/redis/errors.go b/db/skins/redis/errors.go new file mode 100644 index 0000000..f9fc0cb --- /dev/null +++ b/db/skins/redis/errors.go @@ -0,0 +1,12 @@ +package redis + +import "fmt" + +type SkinNotFound struct { + Who string +} + +func (e SkinNotFound) Error() string { + return fmt.Sprintf("Skin data not found. Required username \"%v\"", e.Who) +} + diff --git a/lib/tools/zlib.go b/db/skins/redis/tools.go similarity index 66% rename from lib/tools/zlib.go rename to db/skins/redis/tools.go index 3857052..6de1f52 100644 --- a/lib/tools/zlib.go +++ b/db/skins/redis/tools.go @@ -1,12 +1,17 @@ -package tools +package redis import ( - "io" "bytes" "compress/zlib" + "io" + "strings" ) -func ZlibEncode(str []byte) []byte { +func buildKey(username string) string { + return "username:" + strings.ToLower(username) +} + +func zlibEncode(str []byte) []byte { var buff bytes.Buffer writer := zlib.NewWriter(&buff) writer.Write(str) @@ -15,7 +20,7 @@ func ZlibEncode(str []byte) []byte { return buff.Bytes() } -func ZlibDecode(bts []byte) ([]byte, error) { +func zlibDecode(bts []byte) ([]byte, error) { buff := bytes.NewReader(bts) reader, readError := zlib.NewReader(buff) if readError != nil { diff --git a/glide.lock b/glide.lock index d0a97c7..9daa24b 100644 --- a/glide.lock +++ b/glide.lock @@ -1,26 +1,75 @@ -hash: f6f5dc2f8d1d8077909c7d1f20d235db58ea482023084274c2ad8a5d8fefcbe1 -updated: 2017-06-26T13:29:35.448302526+03:00 +hash: 6fd59478a6c00f45362926d50bc097e2a4ec93fdf2a8105c70d3cdb494ece5d9 +updated: 2017-06-30T18:38:42.231325254+03:00 imports: +- name: github.com/fsnotify/fsnotify + version: 4da3e2cfbabc9f751898f250b49f2439785783a1 - name: github.com/gorilla/context version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 - name: github.com/gorilla/mux version: bcd8bc72b08df0f70df986b97f95590779502d31 +- name: github.com/hashicorp/hcl + version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca + subpackages: + - hcl/ast + - hcl/parser + - hcl/scanner + - hcl/strconv + - hcl/token + - json/parser + - json/scanner + - json/token +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/magiconair/properties + version: 51463bfca2576e06c62a8504b5c0f06d61312647 - name: github.com/mediocregopher/radix.v2 version: dbcfd490034f823788edc555737247e9ba628b6c subpackages: + - cluster - pool - redis + - util +- name: github.com/mitchellh/go-homedir + version: b8bc1bf767474819792c23f32d8286a45736f1c6 +- name: github.com/mitchellh/mapstructure + version: d0303fe809921458f417bcf828397a65db30a7e4 - name: github.com/mono83/slf version: 8188a95c8d6b74c43953abb38b8bd6fdbc412ff5 subpackages: - params - rays - - recievers - recievers/ansi - recievers/statsd - wd -- name: github.com/mono83/udpwriter - version: a064bd7e3acfda563ea680b913b9ef24b7a73e15 +- name: github.com/pelletier/go-toml + version: 69d355db5304c0f7f809a2edc054553e7142f016 +- name: github.com/spf13/afero + version: 9be650865eab0c12963d8753212f4f9c66cdcf12 + subpackages: + - mem +- name: github.com/spf13/cast + version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 +- name: github.com/spf13/cobra + version: 4d647c8944eb42504a714e57e97f244ed6344722 + subpackages: + - cobra +- name: github.com/spf13/jwalterweatherman + version: 0efa5202c04663c757d84f90f5219c1250baf94f +- name: github.com/spf13/pflag + version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 +- name: github.com/spf13/viper + version: c1de95864d73a5465492829d7cb2dd422b19ac96 - name: github.com/streadway/amqp version: 27859d32540aebd2e5befa52dc59ae8e6a0132b6 +- name: golang.org/x/sys + version: 90796e5a05ce440b41c768bd9af257005e470461 + subpackages: + - unix +- name: golang.org/x/text + version: 2bf8f2a19ec09c670e931282edfe6567f6be21c9 + subpackages: + - transform + - unicode/norm +- name: gopkg.in/yaml.v2 + version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b testImports: [] diff --git a/glide.yaml b/glide.yaml index 615d7ed..fcd435e 100644 --- a/glide.yaml +++ b/glide.yaml @@ -15,3 +15,8 @@ import: - recievers/statsd - wd - package: github.com/streadway/amqp +- package: github.com/spf13/cobra + subpackages: + - cobra +- package: github.com/mitchellh/go-homedir +- package: github.com/spf13/viper diff --git a/lib/data/CapeItem.go b/lib/data/CapeItem.go deleted file mode 100644 index 8f03ad8..0000000 --- a/lib/data/CapeItem.go +++ /dev/null @@ -1,43 +0,0 @@ -package data - -import ( - "io" - "os" - "fmt" - "strings" - "crypto/md5" - "encoding/hex" - - "elyby/minecraft-skinsystem/lib/services" -) - -type CapeItem struct { - File *os.File -} - -func FindCapeByUsername(username string) (CapeItem, error) { - var record CapeItem - file, err := os.Open(services.RootFolder + "/data/capes/" + strings.ToLower(username) + ".png") - if (err != nil) { - return record, CapeNotFound{username} - } - - record.File = file - - return record, err -} - -func (cape *CapeItem) CalculateHash() string { - hasher := md5.New() - io.Copy(hasher, cape.File) - - return hex.EncodeToString(hasher.Sum(nil)) -} - -type CapeNotFound struct { - Who string -} - -func (e CapeNotFound) Error() string { - return fmt.Sprintf("Cape file not found. Required username \"%v\"", e.Who) -} diff --git a/lib/data/SignedTexturesResponse.go b/lib/data/SignedTexturesResponse.go deleted file mode 100644 index 4c5eef9..0000000 --- a/lib/data/SignedTexturesResponse.go +++ /dev/null @@ -1,14 +0,0 @@ -package data - -type SignedTexturesResponse struct { - Id string `json:"id"` - Name string `json:"name"` - IsEly bool `json:"ely,omitempty"` - Props []Property `json:"properties"` -} - -type Property struct { - Name string `json:"name"` - Signature string `json:"signature,omitempty"` - Value string `json:"value"` -} diff --git a/lib/data/SkinItem.go b/lib/data/SkinItem.go deleted file mode 100644 index 0e9e208..0000000 --- a/lib/data/SkinItem.go +++ /dev/null @@ -1,117 +0,0 @@ -package data - -import ( - "log" - "fmt" - "encoding/json" - - "elyby/minecraft-skinsystem/lib/services" - "elyby/minecraft-skinsystem/lib/tools" - - "github.com/mediocregopher/radix.v2/redis" -) - -type SkinItem struct { - UserId int `json:"userId"` - Uuid string `json:"uuid"` - Username string `json:"username"` - SkinId int `json:"skinId"` - Url string `json:"url"` - Is1_8 bool `json:"is1_8"` - IsSlim bool `json:"isSlim"` - Hash string `json:"hash"` - MojangTextures string `json:"mojangTextures"` - MojangSignature string `json:"mojangSignature"` - oldUsername string -} - -const accountIdToUsernameKey string = "hash:username-to-account-id" - -func (s *SkinItem) Save() { - str, _ := json.Marshal(s) - compressedStr := tools.ZlibEncode(str) - pool, _ := services.RedisPool.Get() - pool.Cmd("MULTI") - - // Если пользователь сменил ник, то мы должны удать его ключ - if (s.oldUsername != "" && s.oldUsername != s.Username) { - pool.Cmd("DEL", tools.BuildKey(s.oldUsername)) - } - - // Если это новая запись или если пользователь сменил ник, то обновляем значение в хэш-таблице - if (s.oldUsername != "" || s.oldUsername != s.Username) { - pool.Cmd("HSET", accountIdToUsernameKey, s.UserId, s.Username) - } - - pool.Cmd("SET", tools.BuildKey(s.Username), compressedStr) - - pool.Cmd("EXEC") - - s.oldUsername = s.Username -} - -func (s *SkinItem) Delete() { - if (s.oldUsername == "") { - return; - } - - pool, _ := services.RedisPool.Get() - pool.Cmd("MULTI") - - pool.Cmd("DEL", tools.BuildKey(s.oldUsername)) - pool.Cmd("HDEL", accountIdToUsernameKey, s.UserId) - - pool.Cmd("EXEC") -} - -func FindSkinByUsername(username string) (SkinItem, error) { - var record SkinItem; - services.Logger.IncCounter("storage.query", 1) - redisKey := tools.BuildKey(username) - response := services.RedisPool.Cmd("GET", redisKey); - if (response.IsType(redis.Nil)) { - services.Logger.IncCounter("storage.not_found", 1) - return record, SkinNotFound{username} - } - - encodedResult, err := response.Bytes() - if err == nil { - services.Logger.IncCounter("storage.found", 1) - result, err := tools.ZlibDecode(encodedResult) - if err != nil { - log.Println("Cannot uncompress zlib for key " + redisKey) - goto finish - } - - err = json.Unmarshal(result, &record) - if err != nil { - log.Println("Cannot decode record data for key" + redisKey) - goto finish - } - - record.oldUsername = record.Username - } - - finish: - - return record, err -} - -func FindSkinById(id int) (SkinItem, error) { - response := services.RedisPool.Cmd("HGET", accountIdToUsernameKey, id); - if (response.IsType(redis.Nil)) { - return SkinItem{}, SkinNotFound{"unknown"} - } - - username, _ := response.Str() - - return FindSkinByUsername(username) -} - -type SkinNotFound struct { - Who string -} - -func (e SkinNotFound) Error() string { - return fmt.Sprintf("Skin data not found. Required username \"%v\"", e.Who) -} diff --git a/lib/data/TexturesResponse.go b/lib/data/TexturesResponse.go deleted file mode 100644 index 9d2d962..0000000 --- a/lib/data/TexturesResponse.go +++ /dev/null @@ -1,21 +0,0 @@ -package data - -type TexturesResponse struct { - Skin *Skin `json:"SKIN"` - Cape *Cape `json:"CAPE,omitempty"` -} - -type Skin struct { - Url string `json:"url"` - Hash string `json:"hash"` - Metadata *SkinMetadata `json:"metadata,omitempty"` -} - -type SkinMetadata struct { - Model string `json:"model"` -} - -type Cape struct { - Url string `json:"url"` - Hash string `json:"hash"` -} diff --git a/lib/external/accounts/AccountInfo.go b/lib/external/accounts/AccountInfo.go deleted file mode 100644 index eb1bb71..0000000 --- a/lib/external/accounts/AccountInfo.go +++ /dev/null @@ -1,44 +0,0 @@ -package accounts - -import ( - "net/http" - "io/ioutil" - "encoding/json" -) - -type AccountInfoResponse struct { - Id int `json:"id"` - Uuid string `json:"uuid"` - Username string `json:"username"` - Email string `json:"email"` -} - -const internalAccountInfoUrl = domain + "/api/internal/accounts/info" - -func (token *Token) AccountInfo(attribute string, value string) (AccountInfoResponse, error) { - request, err := http.NewRequest("GET", internalAccountInfoUrl, nil) - request.Header.Add("Authorization", "Bearer " + token.AccessToken) - query := request.URL.Query() - query.Add(attribute, value) - request.URL.RawQuery = query.Encode() - - response, err := Client.Do(request) - if err != nil { - panic(err) - } - - defer response.Body.Close() - - var info AccountInfoResponse - - responseError := handleResponse(response) - if responseError != nil { - return info, responseError - } - - body, _ := ioutil.ReadAll(response.Body) - println("Raw account info response is " + string(body)) - json.Unmarshal(body, &info) - - return info, nil -} diff --git a/lib/external/accounts/GetToken.go b/lib/external/accounts/GetToken.go deleted file mode 100644 index 49afd5c..0000000 --- a/lib/external/accounts/GetToken.go +++ /dev/null @@ -1,49 +0,0 @@ -package accounts - -import ( - "strings" - "net/url" - "io/ioutil" - "encoding/json" -) - -type TokenRequest struct { - Id string - Secret string - Scopes []string -} - -type Token struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` -} - -const tokenUrl = domain + "/api/oauth2/v1/token" - -func GetToken(request TokenRequest) (Token, error) { - form := url.Values{} - form.Add("client_id", request.Id) - form.Add("client_secret", request.Secret) - form.Add("grant_type", "client_credentials") - form.Add("scope", strings.Join(request.Scopes, ",")) - - response, err := Client.Post(tokenUrl, "application/x-www-form-urlencoded", strings.NewReader(form.Encode())) - if err != nil { - panic(err) - } - - defer response.Body.Close() - - var result Token - responseError := handleResponse(response) - if responseError != nil { - return result, responseError - } - - body, _ := ioutil.ReadAll(response.Body) - - json.Unmarshal(body, &result) - - return result, nil -} diff --git a/lib/external/accounts/base.go b/lib/external/accounts/base.go deleted file mode 100644 index b72a9d1..0000000 --- a/lib/external/accounts/base.go +++ /dev/null @@ -1,51 +0,0 @@ -package accounts - -import ( - "fmt" - "net/http" -) - -const domain = "https://account.ely.by" - -var Client = &http.Client{} - -type UnauthorizedResponse struct {} - -func (err UnauthorizedResponse) Error() string { - return "Unauthorized response" -} - -type ForbiddenResponse struct {} - -func (err ForbiddenResponse) Error() string { - return "Forbidden response" -} - -type NotFoundResponse struct {} - -func (err NotFoundResponse) Error() string { - return "Not found" -} - -type NotSuccessResponse struct { - StatusCode int -} - -func (err NotSuccessResponse) Error() string { - return fmt.Sprintf("Response code is \"%d\"", err.StatusCode) -} - -func handleResponse(response *http.Response) error { - switch status := response.StatusCode; status { - case 200: - return nil - case 401: - return &UnauthorizedResponse{} - case 403: - return &ForbiddenResponse{} - case 404: - return &NotFoundResponse{} - default: - return &NotSuccessResponse{status} - } -} diff --git a/lib/routes/Cape.go b/lib/routes/Cape.go deleted file mode 100644 index d0560ca..0000000 --- a/lib/routes/Cape.go +++ /dev/null @@ -1,40 +0,0 @@ -package routes - -import ( - "io" - "net/http" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/lib/tools" - "elyby/minecraft-skinsystem/lib/data" - "elyby/minecraft-skinsystem/lib/services" -) - -func Cape(response http.ResponseWriter, request *http.Request) { - if (mux.Vars(request)["converted"] == "") { - services.Logger.IncCounter("capes.request", 1) - } - - username := tools.ParseUsername(mux.Vars(request)["username"]) - rec, err := data.FindCapeByUsername(username) - if (err != nil) { - http.Redirect(response, request, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301) - } - - request.Header.Set("Content-Type", "image/png") - io.Copy(response, rec.File) -} - -func CapeGET(w http.ResponseWriter, r *http.Request) { - services.Logger.IncCounter("capes.get_request", 1) - username := r.URL.Query().Get("name") - if username == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - mux.Vars(r)["username"] = username - mux.Vars(r)["converted"] = "1" - Cape(w, r) -} diff --git a/lib/routes/Face.go b/lib/routes/Face.go deleted file mode 100644 index 24a662d..0000000 --- a/lib/routes/Face.go +++ /dev/null @@ -1,29 +0,0 @@ -package routes - -import ( - "net/http" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/lib/tools" - "elyby/minecraft-skinsystem/lib/data" -) - -const defaultHash = "default" - -func Face(w http.ResponseWriter, r *http.Request) { - username := tools.ParseUsername(mux.Vars(r)["username"]) - rec, err := data.FindSkinByUsername(username) - var hash string - if (err != nil || rec.SkinId == 0) { - hash = defaultHash; - } else { - hash = rec.Hash - } - - http.Redirect(w, r, tools.BuildElyUrl(buildFaceUrl(hash)), 301); -} - -func buildFaceUrl(hash string) string { - return "/minecraft/skin_buffer/faces/" + hash + ".png" -} diff --git a/lib/routes/MinecraftPHP.go b/lib/routes/MinecraftPHP.go deleted file mode 100644 index dc237df..0000000 --- a/lib/routes/MinecraftPHP.go +++ /dev/null @@ -1,34 +0,0 @@ -package routes - -import ( - "net/http" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/lib/services" -) - -// Метод-наследие от первой версии системы скинов. -// Всё ещё иногда используется -// Просто конвертируем данные и отправляем их в основной обработчик -func MinecraftPHP(w http.ResponseWriter, r *http.Request) { - username := r.URL.Query().Get("name") - required := r.URL.Query().Get("type") - if username == "" || required == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - mux.Vars(r)["username"] = username - mux.Vars(r)["converted"] = "1" - switch required { - case "skin": - services.Logger.IncCounter("skins.minecraft-php-request", 1) - Skin(w, r) - case "cloack": - services.Logger.IncCounter("capes.minecraft-php-request", 1) - Cape(w, r) - default: - w.WriteHeader(http.StatusNotFound) - } -} diff --git a/lib/routes/NotFound.go b/lib/routes/NotFound.go deleted file mode 100644 index 9cd1a44..0000000 --- a/lib/routes/NotFound.go +++ /dev/null @@ -1,18 +0,0 @@ -package routes - -import ( - "net/http" - "encoding/json" -) - -func NotFound(w http.ResponseWriter, r *http.Request) { - json, _ := json.Marshal(map[string]string{ - "status": "404", - "message": "Not Found", - "link": "http://docs.ely.by/skin-system.html", - }) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - w.Write(json) -} diff --git a/lib/routes/SignedTextures.go b/lib/routes/SignedTextures.go deleted file mode 100644 index 916042b..0000000 --- a/lib/routes/SignedTextures.go +++ /dev/null @@ -1,44 +0,0 @@ -package routes - -import ( - "strings" - "net/http" - "encoding/json" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/lib/data" - "elyby/minecraft-skinsystem/lib/tools" - "elyby/minecraft-skinsystem/lib/services" -) - -func SignedTextures(w http.ResponseWriter, r *http.Request) { - services.Logger.IncCounter("signed_textures.request", 1) - username := tools.ParseUsername(mux.Vars(r)["username"]) - - rec, err := data.FindSkinByUsername(username) - if (err != nil || rec.SkinId == 0 || rec.MojangTextures == "") { - w.WriteHeader(http.StatusNoContent) - return - } - - responseData:= data.SignedTexturesResponse{ - Id: strings.Replace(rec.Uuid, "-", "", -1), - Name: rec.Username, - Props: []data.Property{ - { - Name: "textures", - Signature: rec.MojangSignature, - Value: rec.MojangTextures, - }, - { - Name: "ely", - Value: "but why are you asking?", - }, - }, - } - - response,_ := json.Marshal(responseData) - w.Header().Set("Content-Type", "application/json") - w.Write(response) -} diff --git a/lib/routes/Skin.go b/lib/routes/Skin.go deleted file mode 100644 index 49f3a31..0000000 --- a/lib/routes/Skin.go +++ /dev/null @@ -1,39 +0,0 @@ -package routes - -import ( - "net/http" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/lib/tools" - "elyby/minecraft-skinsystem/lib/data" - "elyby/minecraft-skinsystem/lib/services" -) - -func Skin(w http.ResponseWriter, r *http.Request) { - if (mux.Vars(r)["converted"] == "") { - services.Logger.IncCounter("skins.request", 1) - } - - username := tools.ParseUsername(mux.Vars(r)["username"]) - rec, err := data.FindSkinByUsername(username) - if (err != nil) { - http.Redirect(w, r, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301) - return - } - - http.Redirect(w, r, tools.BuildElyUrl(rec.Url), 301); -} - -func SkinGET(w http.ResponseWriter, r *http.Request) { - services.Logger.IncCounter("skins.get_request", 1) - username := r.URL.Query().Get("name") - if username == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - mux.Vars(r)["username"] = username - mux.Vars(r)["converted"] = "1" - Skin(w, r) -} diff --git a/lib/routes/Textures.go b/lib/routes/Textures.go deleted file mode 100644 index 7d324fb..0000000 --- a/lib/routes/Textures.go +++ /dev/null @@ -1,61 +0,0 @@ -package routes - -import ( - "log" - "net/http" - "encoding/json" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/lib/data" - "elyby/minecraft-skinsystem/lib/tools" - "elyby/minecraft-skinsystem/lib/services" -) - -func Textures(w http.ResponseWriter, r *http.Request) { - services.Logger.IncCounter("textures.request", 1) - username := tools.ParseUsername(mux.Vars(r)["username"]) - - rec, err := data.FindSkinByUsername(username) - if (err != nil || rec.SkinId == 0) { - rec.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png" - rec.Hash = string(tools.BuildNonElyTexturesHash(username)) - } else { - rec.Url = tools.BuildElyUrl(rec.Url) - } - - textures := data.TexturesResponse{ - Skin: &data.Skin{ - Url: rec.Url, - Hash: rec.Hash, - }, - } - - if (rec.IsSlim) { - textures.Skin.Metadata = &data.SkinMetadata{ - Model: "slim", - } - } - - capeRec, err := data.FindCapeByUsername(username) - if (err == nil) { - capeUrl, err := services.Router.Get("cloaks").URL("username", username) - if (err != nil) { - log.Println(err.Error()) - } - - var scheme string = "http://"; - if (r.TLS != nil) { - scheme = "https://" - } - - textures.Cape = &data.Cape{ - Url: scheme + r.Host + capeUrl.String(), - Hash: capeRec.CalculateHash(), - } - } - - response,_ := json.Marshal(textures) - w.Header().Set("Content-Type", "application/json") - w.Write(response) -} diff --git a/lib/services/services.go b/lib/services/services.go deleted file mode 100644 index 07e8c70..0000000 --- a/lib/services/services.go +++ /dev/null @@ -1,18 +0,0 @@ -package services - -import ( - "github.com/mediocregopher/radix.v2/pool" - "github.com/streadway/amqp" - "github.com/gorilla/mux" - "github.com/mono83/slf/wd" -) - -var Router *mux.Router - -var RedisPool *pool.Pool - -var RabbitMQChannel *amqp.Channel - -var RootFolder string - -var Logger wd.Watchdog diff --git a/lib/tools/tools_test.go b/lib/tools/tools_test.go deleted file mode 100644 index cb6fd37..0000000 --- a/lib/tools/tools_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package tools_test - -import ( - "testing" - . "elyby/minecraft-skinsystem/lib/tools" -) - -func TestParseUsername(t *testing.T) { - if ParseUsername("test.png") != "test" { - t.Error("Function should trim .png at end") - } - - if ParseUsername("test") != "test" { - t.Error("Function should return string itself, if it not contains .png at end") - } -} - -func TestBuildKey(t *testing.T) { - if BuildKey("Test") != "username:test" { - t.Error("Function shound convert string to lower case and concatenate it with usernmae:") - } -} - -func TestBuildElyUrl(t *testing.T) { - if BuildElyUrl("/route") != "http://ely.by/route" { - t.Error("Function should add prefix to the provided relative url.") - } - - if BuildElyUrl("http://ely.by/test/route") != "http://ely.by/test/route" { - t.Error("Function should do not add prefix to the provided prefixed url.") - } -} diff --git a/lib/worker/handlers.go b/lib/worker/handlers.go deleted file mode 100644 index fc3c32d..0000000 --- a/lib/worker/handlers.go +++ /dev/null @@ -1,82 +0,0 @@ -package worker - -import ( - "fmt" - "elyby/minecraft-skinsystem/lib/data" - "elyby/minecraft-skinsystem/lib/services" -) - -func handleChangeUsername(model usernameChanged) (bool) { - if (model.OldUsername == "") { - services.Logger.IncCounter("worker.change_username.empty_old_username", 1) - record := data.SkinItem{ - UserId: model.AccountId, - Username: model.NewUsername, - } - - record.Save() - - return true - } - - record, err := data.FindSkinById(model.AccountId) - if (err != nil) { - services.Logger.IncCounter("worker.change_username.id_not_found", 1) - fmt.Println("Cannot find user id. Trying to search.") - response, err := getById(model.AccountId) - if err != nil { - services.Logger.IncCounter("worker.change_username.id_not_restored", 1) - fmt.Printf("Cannot restore user info. %T\n", err) - // TODO: логгировать в какой-нибудь Sentry, если там не 404 - return true - } - - services.Logger.IncCounter("worker.change_username.id_restored", 1) - fmt.Println("User info successfully restored.") - record = data.SkinItem{ - UserId: response.Id, - } - } - - record.Username = model.NewUsername - record.Save() - - services.Logger.IncCounter("worker.change_username.processed", 1) - - return true -} - -func handleSkinChanged(model skinChanged) bool { - record, err := data.FindSkinById(model.AccountId) - if err != nil { - services.Logger.IncCounter("worker.skin_changed.id_not_found", 1) - fmt.Println("Cannot find user id. Trying to search.") - response, err := getById(model.AccountId) - if err != nil { - services.Logger.IncCounter("worker.skin_changed.id_not_restored", 1) - fmt.Printf("Cannot restore user info. %T\n", err) - // TODO: логгировать в какой-нибудь Sentry, если там не 404 - return true - } - - services.Logger.IncCounter("worker.skin_changed.id_restored", 1) - fmt.Println("User info successfully restored.") - record.UserId = response.Id - record.Username = response.Username - } - - record.Uuid = model.Uuid - record.SkinId = model.SkinId - record.Hash = model.Hash - record.Is1_8 = model.Is1_8 - record.IsSlim = model.IsSlim - record.Url = model.Url - record.MojangTextures = model.MojangTextures - record.MojangSignature = model.MojangSignature - - record.Save() - - services.Logger.IncCounter("worker.skin_changed.processed", 1) - - return true -} diff --git a/lib/worker/supports.go b/lib/worker/supports.go deleted file mode 100644 index beb8aea..0000000 --- a/lib/worker/supports.go +++ /dev/null @@ -1,60 +0,0 @@ -package worker - -import ( - "strconv" - "elyby/minecraft-skinsystem/lib/external/accounts" -) - -var AccountsTokenConfig *accounts.TokenRequest - -var token *accounts.Token - -const repeatsLimit = 3 -var repeatsCount = 0 - -func getById(id int) (accounts.AccountInfoResponse, error) { - return _getByField("id", strconv.Itoa(id)) -} - -func _getByField(field string, value string) (accounts.AccountInfoResponse, error) { - defer resetRepeatsCount() - - apiToken, err := getToken() - if err != nil { - return accounts.AccountInfoResponse{}, err - } - - result, err := apiToken.AccountInfo(field, value) - if err != nil { - _, ok := err.(*accounts.UnauthorizedResponse) - if !ok || repeatsCount >= repeatsLimit { - return accounts.AccountInfoResponse{}, err - } - - repeatsCount++ - token = nil - - return _getByField(field, value) - } - - return result, nil -} - -func getToken() (*accounts.Token, error) { - if token == nil { - println("token is nil, trying to obtain new one") - tempToken, err := accounts.GetToken(*AccountsTokenConfig) - if err != nil { - println("cannot obtain new one token", err) - return &accounts.Token{}, err - } - - token = &tempToken - } - - return token, nil -} - -func resetRepeatsCount() { - repeatsCount = 0 -} diff --git a/lib/worker/worker.go b/lib/worker/worker.go deleted file mode 100644 index 27a3407..0000000 --- a/lib/worker/worker.go +++ /dev/null @@ -1,88 +0,0 @@ -package worker - -import ( - "log" - - "encoding/json" - - "elyby/minecraft-skinsystem/lib/services" -) - -const exchangeName string = "events" -const queueName string = "skinsystem-accounts-events" - -func Listen() { - var err error - ch := services.RabbitMQChannel - - err = ch.ExchangeDeclare( - exchangeName, // name - "topic", // type - true, // durable - false, // auto-deleted - false, // internal - false, // no-wait - nil, // arguments - ) - failOnError(err, "Failed to declare an exchange") - - _, err = ch.QueueDeclare( - queueName, // name - true, // durable - false, // delete when usused - false, // exclusive - false, // no-wait - nil, // arguments - ) - failOnError(err, "Failed to declare a queue") - - err = ch.QueueBind(queueName, "accounts.username-changed", exchangeName, false, nil) - failOnError(err, "Failed to bind a queue") - - err = ch.QueueBind(queueName, "accounts.skin-changed", exchangeName, false, nil) - failOnError(err, "Failed to bind a queue") - - msgs, err := ch.Consume( - queueName, // queue - "", // consumer - false, // auto-ack - false, // exclusive - false, // no-local - false, // no-wait - nil, // args - ) - failOnError(err, "Failed to register a consumer") - - forever := make(chan bool) - - go func() { - for d := range msgs { - log.Println("Incoming message with routing key " + d.RoutingKey) - var result bool = true; - switch d.RoutingKey { - case "accounts.username-changed": - var model usernameChanged - json.Unmarshal(d.Body, &model) - result = handleChangeUsername(model) - case "accounts.skin-changed": - var model skinChanged - json.Unmarshal(d.Body, &model) - result = handleSkinChanged(model) - } - - if (result) { - d.Ack(false) - } else { - d.Reject(true) - } - } - }() - - <-forever -} - -func failOnError(err error, msg string) { - if err != nil { - log.Fatalf("%s: %s", msg, err) - } -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ecfb475 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "elyby/minecraft-skinsystem/cmd" + +func main() { + cmd.Execute() +} diff --git a/minecraft-skinsystem.go b/minecraft-skinsystem.go deleted file mode 100644 index 7c2e51c..0000000 --- a/minecraft-skinsystem.go +++ /dev/null @@ -1,147 +0,0 @@ -package main - -import ( - "os" - "log" - "runtime" - "time" - "net/http" - "path/filepath" - - "github.com/gorilla/mux" - "github.com/streadway/amqp" - "github.com/mediocregopher/radix.v2/pool" - "github.com/mono83/slf/wd" - "github.com/mono83/slf/rays" - "github.com/mono83/slf/recievers/ansi" - "github.com/mono83/slf/recievers/statsd" - - "elyby/minecraft-skinsystem/lib/routes" - "elyby/minecraft-skinsystem/lib/services" - "elyby/minecraft-skinsystem/lib/worker" - "elyby/minecraft-skinsystem/lib/external/accounts" -) - -const redisPoolSize int = 10 - -func main() { - log.Println("Starting...") - - runtime.GOMAXPROCS(runtime.NumCPU()) - - accountsApiId := os.Getenv("ACCOUNTS_API_ID") - accountsApiSecret := os.Getenv("ACCOUNTS_API_SECRET") - if accountsApiId == "" || accountsApiSecret == "" { - log.Fatal("ACCOUNTS_API params must be provided") - } - - worker.AccountsTokenConfig = &accounts.TokenRequest{ - Id: accountsApiId, - Secret: accountsApiSecret, - Scopes: []string{ - "internal_account_info", - }, - } - - log.Println("Connecting to redis") - - var redisString = os.Getenv("REDIS_ADDR") - if (redisString == "") { - redisString = "redis:6379" - } - - redisPool, redisErr := pool.New("tcp", redisString, redisPoolSize) - if (redisErr != nil) { - log.Fatal("Redis unavailable") - } - log.Println("Connected to redis") - - log.Println("Connecting to rabbitmq") - // TODO: rabbitmq становится доступен не сразу. Нужно дождаться, пока он станет доступен, периодически повторяя запросы - - var rabbitmqString = os.Getenv("RABBITMQ_ADDR") - if (rabbitmqString == "") { - rabbitmqString = "amqp://ely-skinsystem-app:ely-skinsystem-app-password@rabbitmq:5672/%2fely" - } - - rabbitConnection, rabbitmqErr := amqp.Dial(rabbitmqString) - if (rabbitmqErr != nil) { - log.Fatalf("%s", rabbitmqErr) - } - log.Println("Connected to rabbitmq. Trying to open a channel") - rabbitChannel, rabbitmqErr := rabbitConnection.Channel() - if (rabbitmqErr != nil) { - log.Fatalf("%s", rabbitmqErr) - } - log.Println("Connected to rabbitmq channel") - - // statsd - var statsdString = os.Getenv("STATSD_ADDR") - if (statsdString != "") { - log.Println("Connecting to statsd") - hostname, _ := os.Hostname() - statsdReceiver, err := statsd.NewReceiver(statsd.Config{ - Address: statsdString, - Prefix: "ely.skinsystem." + hostname + ".app.", - FlushEvery: 1, - }) - if (err != nil) { - log.Fatal("statsd connection error") - } - - wd.AddReceiver(statsdReceiver) - } else { - wd.AddReceiver(ansi.New(true, true, false)) - } - - logger := wd.New("", "").WithParams(rays.Host) - - router := mux.NewRouter().StrictSlash(true) - router.HandleFunc("/skins/{username}", routes.Skin).Methods("GET").Name("skins") - router.HandleFunc("/cloaks/{username}", routes.Cape).Methods("GET").Name("cloaks") - router.HandleFunc("/textures/{username}", routes.Textures).Methods("GET").Name("textures") - router.HandleFunc("/textures/signed/{username}", routes.SignedTextures).Methods("GET").Name("signedTextures") - router.HandleFunc("/skins/{username}/face", routes.Face).Methods("GET").Name("faces") - router.HandleFunc("/skins/{username}/face.png", routes.Face).Methods("GET").Name("faces") - // Legacy - router.HandleFunc("/minecraft.php", routes.MinecraftPHP).Methods("GET") - router.HandleFunc("/skins/", routes.SkinGET).Methods("GET") - router.HandleFunc("/cloaks/", routes.CapeGET).Methods("GET") - // 404 - router.NotFoundHandler = http.HandlerFunc(routes.NotFound) - - services.Router = router - services.RedisPool = redisPool - services.RabbitMQChannel = rabbitChannel - services.Logger = logger - - _, file, _, _ := runtime.Caller(0) - services.RootFolder = filepath.Dir(file) - - go func() { - period := 5 - for { - time.Sleep(time.Duration(period) * time.Second) - - resp := services.RedisPool.Cmd("PING") - if (resp.Err == nil) { - // Если редис успешно пинганулся, значит всё хорошо - continue - } - - log.Println("Redis not pinged. Try to reconnect") - newPool, redisErr := pool.New("tcp", redisString, redisPoolSize) - if (redisErr != nil) { - log.Printf("Cannot reconnect to redis, waiting %d seconds\n", period) - } else { - services.RedisPool = newPool - log.Println("Reconnected") - } - } - }() - - go worker.Listen() - - log.Println("Started"); - log.Fatal(http.ListenAndServe(":80", router)) -} diff --git a/model/cape.go b/model/cape.go new file mode 100644 index 0000000..67b84cb --- /dev/null +++ b/model/cape.go @@ -0,0 +1,11 @@ +package model + +import "os" + +type Cape struct { + File *os.File +} + +type CapesRepository interface { + FindByUsername(username string) (Cape, error) +} diff --git a/lib/worker/models.go b/model/skin.go similarity index 54% rename from lib/worker/models.go rename to model/skin.go index 0844112..27858ca 100644 --- a/lib/worker/models.go +++ b/model/skin.go @@ -1,20 +1,20 @@ -package worker +package model -type usernameChanged struct { - AccountId int `json:"accountId"` - OldUsername string `json:"oldUsername"` - NewUsername string `json:"newUsername"` -} - -type skinChanged struct { - AccountId int `json:"userId"` +type Skin struct { + UserId int `json:"userId"` Uuid string `json:"uuid"` + Username string `json:"username"` SkinId int `json:"skinId"` - OldSkinId int `json:"oldSkinId"` - Hash string `json:"hash"` + Url string `json:"url"` Is1_8 bool `json:"is1_8"` IsSlim bool `json:"isSlim"` - Url string `json:"url"` + Hash string `json:"hash"` MojangTextures string `json:"mojangTextures"` MojangSignature string `json:"mojangSignature"` + OldUsername string +} + +type SkinsRepository interface { + FindByUsername(username string) (Skin, error) + FindByUserId(id int) (Skin, error) } diff --git a/ui/cape.go b/ui/cape.go new file mode 100644 index 0000000..22ae151 --- /dev/null +++ b/ui/cape.go @@ -0,0 +1,39 @@ +package ui + +import ( + "io" + "net/http" + + "github.com/gorilla/mux" + + "elyby/minecraft-skinsystem/utils" +) + +func (s *uiService) Cape(response http.ResponseWriter, request *http.Request) { + if mux.Vars(request)["converted"] == "" { + s.logger.IncCounter("capes.request", 1) + } + + username := utils.ParseUsername(mux.Vars(request)["username"]) + rec, err := s.capesRepo.FindByUsername(username) + if err != nil { + http.Redirect(response, request, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301) + } + + request.Header.Set("Content-Type", "image/png") + io.Copy(response, rec.File) +} + +func (s *uiService) CapeGET(response http.ResponseWriter, request *http.Request) { + s.logger.IncCounter("capes.get_request", 1) + username := request.URL.Query().Get("name") + if username == "" { + response.WriteHeader(http.StatusBadRequest) + return + } + + mux.Vars(request)["username"] = username + mux.Vars(request)["converted"] = "1" + + s.Cape(response, request) +} diff --git a/ui/face.go b/ui/face.go new file mode 100644 index 0000000..ea7e4e3 --- /dev/null +++ b/ui/face.go @@ -0,0 +1,28 @@ +package ui + +import ( + "net/http" + + "github.com/gorilla/mux" + + "elyby/minecraft-skinsystem/utils" +) + +const defaultHash = "default" + +func (s *uiService) Face(response http.ResponseWriter, request *http.Request) { + username := utils.ParseUsername(mux.Vars(request)["username"]) + rec, err := s.skinsRepo.FindByUsername(username) + var hash string + if err != nil || rec.SkinId == 0 { + hash = defaultHash + } else { + hash = rec.Hash + } + + http.Redirect(response, request, utils.BuildElyUrl(buildFaceUrl(hash)), 301) +} + +func buildFaceUrl(hash string) string { + return "/minecraft/skin_buffer/faces/" + hash + ".png" +} diff --git a/ui/minecraft_php.go b/ui/minecraft_php.go new file mode 100644 index 0000000..9878a82 --- /dev/null +++ b/ui/minecraft_php.go @@ -0,0 +1,33 @@ +package ui + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +// Метод-наследие от первой версии системы скинов. +// Всё ещё иногда используется +// Просто конвертируем данные и отправляем их в основной обработчик +func (s *uiService) MinecraftPHP(response http.ResponseWriter, request *http.Request) { + username := request.URL.Query().Get("name") + required := request.URL.Query().Get("type") + if username == "" || required == "" { + response.WriteHeader(http.StatusBadRequest) + return + } + + mux.Vars(request)["username"] = username + mux.Vars(request)["converted"] = "1" + + switch required { + case "skin": + s.logger.IncCounter("skins.minecraft-php-request", 1) + s.Skin(response, request) + case "cloack": + s.logger.IncCounter("capes.minecraft-php-request", 1) + s.Cape(response, request) + default: + response.WriteHeader(http.StatusNotFound) + } +} diff --git a/ui/not_found.go b/ui/not_found.go new file mode 100644 index 0000000..7723146 --- /dev/null +++ b/ui/not_found.go @@ -0,0 +1,18 @@ +package ui + +import ( + "encoding/json" + "net/http" +) + +func NotFound(response http.ResponseWriter, request *http.Request) { + json, _ := json.Marshal(map[string]string{ + "status": "404", + "message": "Not Found", + "link": "http://docs.ely.by/skin-system.html", + }) + + response.Header().Set("Content-Type", "application/json") + response.WriteHeader(http.StatusNotFound) + response.Write(json) +} diff --git a/ui/service.go b/ui/service.go new file mode 100644 index 0000000..237ab5c --- /dev/null +++ b/ui/service.go @@ -0,0 +1,25 @@ +package ui + +import ( + "elyby/minecraft-skinsystem/model" + + "github.com/mono83/slf/wd" +) + +type uiService struct { + logger wd.Watchdog + skinsRepo model.SkinsRepository + capesRepo model.CapesRepository +} + +func NewUiService( + logger wd.Watchdog, + skinsRepo model.SkinsRepository, + capesRepo model.CapesRepository, +) (*uiService, error) { + return &uiService{ + logger: logger, + skinsRepo: skinsRepo, + capesRepo: capesRepo, + }, nil +} diff --git a/ui/signed_textures.go b/ui/signed_textures.go new file mode 100644 index 0000000..aff0086 --- /dev/null +++ b/ui/signed_textures.go @@ -0,0 +1,55 @@ +package ui + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/gorilla/mux" + + "elyby/minecraft-skinsystem/utils" +) + +type signedTexturesResponse struct { + Id string `json:"id"` + Name string `json:"name"` + IsEly bool `json:"ely,omitempty"` + Props []property `json:"properties"` +} + +type property struct { + Name string `json:"name"` + Signature string `json:"signature,omitempty"` + Value string `json:"value"` +} + +func (s *uiService) SignedTextures(response http.ResponseWriter, request *http.Request) { + s.logger.IncCounter("signed_textures.request", 1) + username := utils.ParseUsername(mux.Vars(request)["username"]) + + rec, err := s.skinsRepo.FindByUsername(username) + if err != nil || rec.SkinId == 0 || rec.MojangTextures == "" { + response.WriteHeader(http.StatusNoContent) + return + } + + responseData:= signedTexturesResponse{ + Id: strings.Replace(rec.Uuid, "-", "", -1), + Name: rec.Username, + Props: []property{ + { + Name: "textures", + Signature: rec.MojangSignature, + Value: rec.MojangTextures, + }, + { + Name: "ely", + Value: "but why are you asking?", + }, + }, + } + + responseJson,_ := json.Marshal(responseData) + response.Header().Set("Content-Type", "application/json") + response.Write(responseJson) +} diff --git a/ui/skin.go b/ui/skin.go new file mode 100644 index 0000000..8d67f35 --- /dev/null +++ b/ui/skin.go @@ -0,0 +1,38 @@ +package ui + +import ( + "net/http" + + "github.com/gorilla/mux" + + "elyby/minecraft-skinsystem/utils" +) + +func (s *uiService) Skin(response http.ResponseWriter, request *http.Request) { + if mux.Vars(request)["converted"] == "" { + s.logger.IncCounter("skins.request", 1) + } + + username := utils.ParseUsername(mux.Vars(request)["username"]) + rec, err := s.skinsRepo.FindByUsername(username) + if err != nil { + http.Redirect(response, request, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301) + return + } + + http.Redirect(response, request, utils.BuildElyUrl(rec.Url), 301) +} + +func (s *uiService) SkinGET(response http.ResponseWriter, request *http.Request) { + s.logger.IncCounter("skins.get_request", 1) + username := request.URL.Query().Get("name") + if username == "" { + response.WriteHeader(http.StatusBadRequest) + return + } + + mux.Vars(request)["username"] = username + mux.Vars(request)["converted"] = "1" + + s.Skin(response, request) +} diff --git a/ui/textures.go b/ui/textures.go new file mode 100644 index 0000000..d5d089c --- /dev/null +++ b/ui/textures.go @@ -0,0 +1,92 @@ +package ui + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + + "crypto/md5" + "encoding/hex" + "io" + + "elyby/minecraft-skinsystem/model" + "elyby/minecraft-skinsystem/utils" +) + +type texturesResponse struct { + Skin *Skin `json:"SKIN"` + Cape *Cape `json:"CAPE,omitempty"` +} + +type Skin struct { + Url string `json:"url"` + Hash string `json:"hash"` + Metadata *skinMetadata `json:"metadata,omitempty"` +} + +type skinMetadata struct { + Model string `json:"model"` +} + +type Cape struct { + Url string `json:"url"` + Hash string `json:"hash"` +} + +func (s *uiService) Textures(response http.ResponseWriter, request *http.Request) { + s.logger.IncCounter("textures.request", 1) + username := utils.ParseUsername(mux.Vars(request)["username"]) + + skin, err := s.skinsRepo.FindByUsername(username) + if err != nil || skin.SkinId == 0 { + skin.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png" + skin.Hash = string(utils.BuildNonElyTexturesHash(username)) + } else { + skin.Url = utils.BuildElyUrl(skin.Url) + } + + textures := texturesResponse{ + Skin: &Skin{ + Url: skin.Url, + Hash: skin.Hash, + }, + } + + if skin.IsSlim { + textures.Skin.Metadata = &skinMetadata{ + Model: "slim", + } + } + + cape, err := s.capesRepo.FindByUsername(username) + if err == nil { + // capeUrl, err := services.Router.Get("cloaks").URL("username", username) + capeUrl := "/capes/" + username + if err != nil { + s.logger.Error(err.Error()) + } + + var scheme string = "http://" + if request.TLS != nil { + scheme = "https://" + } + + textures.Cape = &Cape{ + // Url: scheme + request.Host + capeUrl.String(), + Url: scheme + request.Host + capeUrl, + Hash: calculateCapeHash(cape), + } + } + + responseData,_ := json.Marshal(textures) + response.Header().Set("Content-Type", "application/json") + response.Write(responseData) +} + +func calculateCapeHash(cape model.Cape) string { + hasher := md5.New() + io.Copy(hasher, cape.File) + + return hex.EncodeToString(hasher.Sum(nil)) +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..77719a0 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,39 @@ +package ui + +import ( + "net" + "net/http" + "time" + + "github.com/gorilla/mux" +) + +type Config struct { + +} + +func Start(cfg Config, s *uiService, lst net.Listener) { + router := mux.NewRouter().StrictSlash(true) + + router.HandleFunc("/skins/{username}", s.Skin).Methods("GET") + router.HandleFunc("/cloaks/{username}", s.Cape).Methods("GET") + router.HandleFunc("/textures/{username}", s.Textures).Methods("GET") + router.HandleFunc("/textures/signed/{username}", s.SignedTextures).Methods("GET") + router.HandleFunc("/skins/{username}/face", s.Face).Methods("GET") + router.HandleFunc("/skins/{username}/face.png", s.Face).Methods("GET") + // Legacy + router.HandleFunc("/minecraft.php", s.MinecraftPHP).Methods("GET") + router.HandleFunc("/skins/", s.SkinGET).Methods("GET") + router.HandleFunc("/cloaks/", s.CapeGET).Methods("GET") + // 404 + router.NotFoundHandler = http.HandlerFunc(NotFound) + + server := &http.Server{ + ReadTimeout: 60 * time.Second, + WriteTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 16, + Handler: router, + } + + go server.Serve(lst) +} diff --git a/lib/tools/tools.go b/utils/utils.go similarity index 86% rename from lib/tools/tools.go rename to utils/utils.go index 96ad886..a550022 100644 --- a/lib/tools/tools.go +++ b/utils/utils.go @@ -1,11 +1,11 @@ -package tools +package utils import ( + "crypto/md5" + "encoding/hex" + "strconv" "strings" "time" - "crypto/md5" - "strconv" - "encoding/hex" ) func ParseUsername(username string) string { @@ -25,10 +25,6 @@ func BuildNonElyTexturesHash(username string) string { return hex.EncodeToString(hasher.Sum(nil)) } -func BuildKey(username string) string { - return "username:" + strings.ToLower(username) -} - func BuildElyUrl(route string) string { prefix := "http://ely.by" if !strings.HasPrefix(route, prefix) { @@ -38,8 +34,10 @@ func BuildElyUrl(route string) string { return route } +var timeNow = time.Now + func getCurrentHour() int64 { - n := time.Now() + n := timeNow() return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix() } diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 0000000..98f7625 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,60 @@ +package utils + +import ( + "testing" + "time" +) + +func TestParseUsername(t *testing.T) { + if ParseUsername("test.png") != "test" { + t.Error("Function should trim .png at end") + } + + if ParseUsername("test") != "test" { + t.Error("Function should return string itself, if it not contains .png at end") + } +} + +func TestBuildNonElyTexturesHash(t *testing.T) { + timeNow = func() time.Time { + return time.Date(2017, time.November, 30, 16, 15, 34, 0, time.UTC) + } + + if BuildNonElyTexturesHash("username") != "686d788a5353cb636e8fdff727634d88" { + t.Error("Function should return fixed hash by username-time pair") + } + + if BuildNonElyTexturesHash("another-username") != "fb876f761683a10accdb17d403cef64c" { + t.Error("Function should return fixed hash by username-time pair") + } + + timeNow = func() time.Time { + return time.Date(2017, time.November, 30, 16, 20, 12, 0, time.UTC) + } + + if BuildNonElyTexturesHash("username") != "686d788a5353cb636e8fdff727634d88" { + t.Error("Function should do not change it's value if hour the same") + } + + if BuildNonElyTexturesHash("another-username") != "fb876f761683a10accdb17d403cef64c" { + t.Error("Function should return fixed hash by username-time pair") + } + + timeNow = func() time.Time { + return time.Date(2017, time.November, 30, 17, 1, 3, 0, time.UTC) + } + + if BuildNonElyTexturesHash("username") != "42277892fd24bc0ed86285b3bb8b8fad" { + t.Error("Function should change it's value if hour changed") + } +} + +func TestBuildElyUrl(t *testing.T) { + if BuildElyUrl("/route") != "http://ely.by/route" { + t.Error("Function should add prefix to the provided relative url.") + } + + if BuildElyUrl("http://ely.by/test/route") != "http://ely.by/test/route" { + t.Error("Function should do not add prefix to the provided prefixed url.") + } +}