Переработка структуры проекта

This commit is contained in:
ErickSkrauch 2017-06-30 18:40:25 +03:00
parent e090d04dc7
commit 07903cf9c8
48 changed files with 894 additions and 1061 deletions

75
cmd/root.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View File

@ -0,0 +1,7 @@
package skins
import "elyby/minecraft-skinsystem/model"
type SkinsRepositoryConfig interface {
CreateRepo() (model.SkinsRepository, error)
}

View 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
View 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
View 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)
}

View File

@ -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 {

59
glide.lock generated
View File

@ -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: []

View File

@ -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

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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}
}
}

View File

@ -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)
}

View File

@ -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"
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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.")
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
View File

@ -0,0 +1,7 @@
package main
import "elyby/minecraft-skinsystem/cmd"
func main() {
cmd.Execute()
}

View File

@ -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
View File

@ -0,0 +1,11 @@
package model
import "os"
type Cape struct {
File *os.File
}
type CapesRepository interface {
FindByUsername(username string) (Cape, error)
}

View File

@ -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)
}

39
ui/cape.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View File

@ -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()
}

60
utils/utils_test.go Normal file
View 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.")
}
}