mirror of
https://github.com/elyby/chrly.git
synced 2024-12-22 21:19:55 +05:30
Переработка структуры проекта
This commit is contained in:
parent
e090d04dc7
commit
07903cf9c8
75
cmd/root.go
Normal file
75
cmd/root.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
}
|
78
cmd/serve.go
Normal file
78
cmd/serve.go
Normal file
@ -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)
|
||||||
|
}
|
54
daemon/http.go
Normal file
54
daemon/http.go
Normal file
@ -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))
|
||||||
|
}
|
7
db/capes/config.go
Normal file
7
db/capes/config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package capes
|
||||||
|
|
||||||
|
import "elyby/minecraft-skinsystem/model"
|
||||||
|
|
||||||
|
type CapesRepositoryConfig interface {
|
||||||
|
CreateRepo() (model.CapesRepository, error)
|
||||||
|
}
|
11
db/capes/files/db.go
Normal file
11
db/capes/files/db.go
Normal file
@ -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
|
||||||
|
}
|
11
db/capes/files/errors.go
Normal file
11
db/capes/files/errors.go
Normal file
@ -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)
|
||||||
|
}
|
26
db/capes/files/repository.go
Normal file
26
db/capes/files/repository.go
Normal file
@ -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
|
||||||
|
}
|
7
db/skins/config.go
Normal file
7
db/skins/config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package skins
|
||||||
|
|
||||||
|
import "elyby/minecraft-skinsystem/model"
|
||||||
|
|
||||||
|
type SkinsRepositoryConfig interface {
|
||||||
|
CreateRepo() (model.SkinsRepository, error)
|
||||||
|
}
|
58
db/skins/redis/commands.go
Normal file
58
db/skins/redis/commands.go
Normal file
@ -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)
|
||||||
|
}
|
23
db/skins/redis/db.go
Normal file
23
db/skins/redis/db.go
Normal file
@ -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
|
||||||
|
}
|
12
db/skins/redis/errors.go
Normal file
12
db/skins/redis/errors.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,17 @@
|
|||||||
package tools
|
package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/zlib"
|
"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
|
var buff bytes.Buffer
|
||||||
writer := zlib.NewWriter(&buff)
|
writer := zlib.NewWriter(&buff)
|
||||||
writer.Write(str)
|
writer.Write(str)
|
||||||
@ -15,7 +20,7 @@ func ZlibEncode(str []byte) []byte {
|
|||||||
return buff.Bytes()
|
return buff.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ZlibDecode(bts []byte) ([]byte, error) {
|
func zlibDecode(bts []byte) ([]byte, error) {
|
||||||
buff := bytes.NewReader(bts)
|
buff := bytes.NewReader(bts)
|
||||||
reader, readError := zlib.NewReader(buff)
|
reader, readError := zlib.NewReader(buff)
|
||||||
if readError != nil {
|
if readError != nil {
|
59
glide.lock
generated
59
glide.lock
generated
@ -1,26 +1,75 @@
|
|||||||
hash: f6f5dc2f8d1d8077909c7d1f20d235db58ea482023084274c2ad8a5d8fefcbe1
|
hash: 6fd59478a6c00f45362926d50bc097e2a4ec93fdf2a8105c70d3cdb494ece5d9
|
||||||
updated: 2017-06-26T13:29:35.448302526+03:00
|
updated: 2017-06-30T18:38:42.231325254+03:00
|
||||||
imports:
|
imports:
|
||||||
|
- name: github.com/fsnotify/fsnotify
|
||||||
|
version: 4da3e2cfbabc9f751898f250b49f2439785783a1
|
||||||
- name: github.com/gorilla/context
|
- name: github.com/gorilla/context
|
||||||
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
|
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
|
||||||
- name: github.com/gorilla/mux
|
- name: github.com/gorilla/mux
|
||||||
version: bcd8bc72b08df0f70df986b97f95590779502d31
|
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
|
- name: github.com/mediocregopher/radix.v2
|
||||||
version: dbcfd490034f823788edc555737247e9ba628b6c
|
version: dbcfd490034f823788edc555737247e9ba628b6c
|
||||||
subpackages:
|
subpackages:
|
||||||
|
- cluster
|
||||||
- pool
|
- pool
|
||||||
- redis
|
- redis
|
||||||
|
- util
|
||||||
|
- name: github.com/mitchellh/go-homedir
|
||||||
|
version: b8bc1bf767474819792c23f32d8286a45736f1c6
|
||||||
|
- name: github.com/mitchellh/mapstructure
|
||||||
|
version: d0303fe809921458f417bcf828397a65db30a7e4
|
||||||
- name: github.com/mono83/slf
|
- name: github.com/mono83/slf
|
||||||
version: 8188a95c8d6b74c43953abb38b8bd6fdbc412ff5
|
version: 8188a95c8d6b74c43953abb38b8bd6fdbc412ff5
|
||||||
subpackages:
|
subpackages:
|
||||||
- params
|
- params
|
||||||
- rays
|
- rays
|
||||||
- recievers
|
|
||||||
- recievers/ansi
|
- recievers/ansi
|
||||||
- recievers/statsd
|
- recievers/statsd
|
||||||
- wd
|
- wd
|
||||||
- name: github.com/mono83/udpwriter
|
- name: github.com/pelletier/go-toml
|
||||||
version: a064bd7e3acfda563ea680b913b9ef24b7a73e15
|
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
|
- name: github.com/streadway/amqp
|
||||||
version: 27859d32540aebd2e5befa52dc59ae8e6a0132b6
|
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: []
|
testImports: []
|
||||||
|
@ -15,3 +15,8 @@ import:
|
|||||||
- recievers/statsd
|
- recievers/statsd
|
||||||
- wd
|
- wd
|
||||||
- package: github.com/streadway/amqp
|
- package: github.com/streadway/amqp
|
||||||
|
- package: github.com/spf13/cobra
|
||||||
|
subpackages:
|
||||||
|
- cobra
|
||||||
|
- package: github.com/mitchellh/go-homedir
|
||||||
|
- package: github.com/spf13/viper
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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"`
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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"`
|
|
||||||
}
|
|
44
lib/external/accounts/AccountInfo.go
vendored
44
lib/external/accounts/AccountInfo.go
vendored
@ -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
|
|
||||||
}
|
|
49
lib/external/accounts/GetToken.go
vendored
49
lib/external/accounts/GetToken.go
vendored
@ -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
|
|
||||||
}
|
|
51
lib/external/accounts/base.go
vendored
51
lib/external/accounts/base.go
vendored
@ -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}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
@ -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.")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
7
main.go
Normal file
7
main.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "elyby/minecraft-skinsystem/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
@ -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))
|
|
||||||
}
|
|
11
model/cape.go
Normal file
11
model/cape.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type Cape struct {
|
||||||
|
File *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
type CapesRepository interface {
|
||||||
|
FindByUsername(username string) (Cape, error)
|
||||||
|
}
|
@ -1,20 +1,20 @@
|
|||||||
package worker
|
package model
|
||||||
|
|
||||||
type usernameChanged struct {
|
type Skin struct {
|
||||||
AccountId int `json:"accountId"`
|
UserId int `json:"userId"`
|
||||||
OldUsername string `json:"oldUsername"`
|
|
||||||
NewUsername string `json:"newUsername"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type skinChanged struct {
|
|
||||||
AccountId int `json:"userId"`
|
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
|
Username string `json:"username"`
|
||||||
SkinId int `json:"skinId"`
|
SkinId int `json:"skinId"`
|
||||||
OldSkinId int `json:"oldSkinId"`
|
Url string `json:"url"`
|
||||||
Hash string `json:"hash"`
|
|
||||||
Is1_8 bool `json:"is1_8"`
|
Is1_8 bool `json:"is1_8"`
|
||||||
IsSlim bool `json:"isSlim"`
|
IsSlim bool `json:"isSlim"`
|
||||||
Url string `json:"url"`
|
Hash string `json:"hash"`
|
||||||
MojangTextures string `json:"mojangTextures"`
|
MojangTextures string `json:"mojangTextures"`
|
||||||
MojangSignature string `json:"mojangSignature"`
|
MojangSignature string `json:"mojangSignature"`
|
||||||
|
OldUsername string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkinsRepository interface {
|
||||||
|
FindByUsername(username string) (Skin, error)
|
||||||
|
FindByUserId(id int) (Skin, error)
|
||||||
}
|
}
|
39
ui/cape.go
Normal file
39
ui/cape.go
Normal file
@ -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)
|
||||||
|
}
|
28
ui/face.go
Normal file
28
ui/face.go
Normal file
@ -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"
|
||||||
|
}
|
33
ui/minecraft_php.go
Normal file
33
ui/minecraft_php.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
18
ui/not_found.go
Normal file
18
ui/not_found.go
Normal file
@ -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)
|
||||||
|
}
|
25
ui/service.go
Normal file
25
ui/service.go
Normal file
@ -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
|
||||||
|
}
|
55
ui/signed_textures.go
Normal file
55
ui/signed_textures.go
Normal file
@ -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)
|
||||||
|
}
|
38
ui/skin.go
Normal file
38
ui/skin.go
Normal file
@ -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)
|
||||||
|
}
|
92
ui/textures.go
Normal file
92
ui/textures.go
Normal file
@ -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))
|
||||||
|
}
|
39
ui/ui.go
Normal file
39
ui/ui.go
Normal file
@ -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)
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
package tools
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"crypto/md5"
|
|
||||||
"strconv"
|
|
||||||
"encoding/hex"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseUsername(username string) string {
|
func ParseUsername(username string) string {
|
||||||
@ -25,10 +25,6 @@ func BuildNonElyTexturesHash(username string) string {
|
|||||||
return hex.EncodeToString(hasher.Sum(nil))
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildKey(username string) string {
|
|
||||||
return "username:" + strings.ToLower(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildElyUrl(route string) string {
|
func BuildElyUrl(route string) string {
|
||||||
prefix := "http://ely.by"
|
prefix := "http://ely.by"
|
||||||
if !strings.HasPrefix(route, prefix) {
|
if !strings.HasPrefix(route, prefix) {
|
||||||
@ -38,8 +34,10 @@ func BuildElyUrl(route string) string {
|
|||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var timeNow = time.Now
|
||||||
|
|
||||||
func getCurrentHour() int64 {
|
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()
|
return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix()
|
||||||
}
|
}
|
||||||
|
|
60
utils/utils_test.go
Normal file
60
utils/utils_test.go
Normal file
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user